<数据结构与算法分析>读书笔记--运行时间中的对数及其分析结果的准确性

分析算法最混乱的方面大概集中在对数上面。我们已经看到,某些分治算法将以O(N log N)时间运行。此外,对数最常出现的规律可概括为下列一般法则:

如果一个算法用常数时间(O(1))将问题的大小削减为其一部分(通常是1/2),那么该算法就是O(logN)。另一方面,如果使用常数时间只是把问题减少一个常数的数量(如将问题减少1),那么这种算法就是O(N)的。

下面,提供了具有对数特点的三个例子,分别为如下:

1.折半查找;

2.欧几里得算法;

3.幂运算;

一、折半查找

第一个例子通常叫做折半查找。

折半查找:给定一个整数X和整数A0,A1,.....,AN-1,后者已经预先排序并在内存中,求下标i使得Ai =X,如果X不在数据中,则返回i=-1。

明显的解法是从左到右扫描数据,其运行花费线性时间。然而,这个算法没有用到该表已经排序的事实,这就使得算法很可能不是最好的。一个好的策略是验证X是否是居中的元素。

如果是,则答案就找到了。如果X小于居中元素,那么我们可以应用同样的策略于居中元素左边已排序的子序列;同理,如果X大于居中元素,那么我们检查数据的右半部分。(同样,也存在可能会终止的情况)。

示例一(反映了Java语言数据下标从0开始的惯例):

public static <AnyType extends Comparable<? super AnyType>> int binarySearch(AnyType[] a,AnyType x) {
        int low = 0,high = a.length-1;
        
        while(low <=high) {
            
            
            int mid = (low+high)/2;
            
            if(a[mid].compareTo(x) < 0)
                low = mid + 1else if (a[mid].compareTo(x) > 0)
                high = mid - 1else 
                return mid;
        }
        
        return -1;//NOT_FOUND
    }
    

 

显然,每次迭代在循环内的所有工作花费O(1),因此分析需要确定循环的次数。循环从high-low=N-1开始,并保持high-low >= -1。

每次循环后high-low的值至少将该次循环前的值折半;于是,循环的次数最多为[log(N-1)]+2。(例如,若high-low=128,则在各次迭代后high-low的最大值是64,32,16,8,4,2,1,0,-1。)因此,运行时间是O(logN)。与此等价,我们也可以写出运行时间的递推公式,不过,当我们理解实际在做什么以及为什么的原理时,这种强行写公式的做法通常没有必要。

折半查找可以看作是我们的第一个数据结构实现方法,它提供了在O(logN)时间内的contains操作,但是所有其他操作(特别是insert操作)均需要O(N)时间。在数据是稳定(即不允许插入操作和删除操作)的应用中,这种操作可能是非常有用的。此时输入数据需要一次排序,但是此后的访问会很快。有个例子是一个程序,它需要保留(产生于化学和物理领域的)元素周期表的信息。这个表是相对稳定的,因为很少会加进新的元素。元素名可以始终是排序的。由于只有大约110种元素,因此找出一个元素最多需要访问8次。要是执行顺序查找就会需要多得多的访问次数。

 

二、欧几里得算法

第二个例子是计算最大公因数的欧几里得算法。两个整数的最大公因数(gcd)是同时整除二者的最大整数。于是,gcd(50,15)=5。

示例二(所示的算法计算gcd(M,N),假设M>=N(如果N>M,则循环的第一次迭代将它们互相交换):

    static long gcd(long m,long n) {
        while( n != 0) {
            
            long rem = m % n;
            m = n;
            n = rem;
        }
        
         m;
    }

 

算法连续计算余数直到余数是0为止,最后的非零余数就是最大公因数。因此,如果M=1989和N=1590,则余数序列是399,393,6,3,0。从而,gcd(1989,1590)=3。正如例子所表明的,这是一个快速算法。

如前所述,估计算法的整个运行时间依赖于确定余数序列究竟有多长。虽然logN看似像理想中的答案,但是根本看不出余数的值按照常数因子递减的必然性,因为我们看到,例中的余数从399仅仅降到393事实上,在一次迭代中余数并不按照一个常数因子递减。然而,我们可以证明,在两次迭代以后,余数最多是原始值的一半。这就证明了,迭代次数至多2 log N = O(logN)从而得到运行时间。这个证明并不难,因此我们将它放在这里,可从下列定理直接推出它。

定理2.1 

如果M>N,则M mod N < M/2。

证明:

存在两种情形。如果N<=M/2,则由于余数小于N,故定理在这种情形下成立。另一种情形是N>M/2。但是此时M仅含有一个N从而余数为M-N < M/2,定理得证。

从上面的例子来看,2 log N 大约为20,而我们仅进行了7次运算,因此有人会怀疑这是不是可能的最好的界。事实上,这个常数在最坏的情况下还可以稍微改进1.44 log N(如M和N是两个相邻的斐波那契数时就是这种情况)。欧几里得算法在平均情况下的性能需要大量篇幅的高度复杂的数学分析,其迭代的平均次数约为(12 ln 2 lnN)/π2 + 1.47。

 

三、幂运算

最后一个例子是处理一个整数的幂(它还是一个整数)。由取幂运算得到的数一般都是相当大的,因此,我们只能在假设有一台机器能够存储这样一些大整数(或有一个编译程序能够模拟它)的情况下进行我们的分析。我们将用乘法的次数作为运行时间的度量。

计算XN 的明显的算法是使用N-1次乘法自乘。有一种递归算法效果较好。N<=1是这种递归的基准情形。否则,若N是偶数,我们有XN = XN/2 . XN/2 ,如果N是奇数,则XN = X(N-1)/2 .

X(N-1)/2 .X。

例如,为了计算X62,算法将如下进行,它只用到9次乘法:

X3 = (X3)X ,X7 = (X3)2X,X15 = (X7)2X,X31 = (X152X,X62 = (X31)2

显然,所需要的乘法次数最多是2logN,因为把问题分半最多需要两次乘法(如果N是奇数)。

这里,我们又可以写出一个递推公式并将其解出。简单的直觉避免了盲目的强行处理。

示例三:

long pow(long x,1)"> n) {
        
        if( n == 0)
            return 1;
        if( n == 1 x;
        if( isEven(n))
            return pow(x * x,n/2);
        else
             x;
    }
    
    boolean isEven(return (n % 2 == 0);
    }

 

关于分析结果的准确性:

根据经验,有时分析会估计过大。如果这种情况发生,那么或者需要进一步细化分析(一般通过机敏的观察),或者可能是平均运行时间显著小于最坏情形的运行时间,不可能对所得的界再加以改进。对于许多复杂的算法,最坏的界通过某个坏的输入是可以达到的,但在实践中情形下仍然悬而未决),而最坏情形的界尽管过分地悲观,但却是最好的已知解析结果。

 

示例源码地址为:https://github.com/youcong1996/The-Data-structures-and-algorithms/tree/master/algorithm_analysis

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设