算法是指解决问题的步骤描述,在计算机中表现为有限的指令序列。
算法具有五个基本特性:
设计一个好的算法需要符合以下几点:
算法的效率主要由以下两个复杂度来评估。
好的算法应该是空间复杂度和时间复杂度都尽可能小,但现实情况往往是鱼和熊掌不可兼得,所以在有时候,我们可以牺牲空间换时间,或是牺牲时间换空间。
由于现代计算机扩展内存相对容易但提升处理器主频较为困难,所以时间复杂度往往比空间复杂度更容易出问题,因此我们在分析算法的复杂度时,一般是指算法的时间复杂度,即算法运行所需要的时间。
算法运行的时间跟很多因素有关,比如CPU的工作频率,问题的输入规模,算法计算过程等。要统计一个算法的运行时间,可以分前事后统计法和事前分析法。事后统计需要通过运行程序才能得出算法的运行时间,可行性比较低,比较依赖计算机硬件和软件因素,所以一般不予采用,而是采用事前分析估算的方法。
事前分析一个算法的运行时间包括以下几个方面的内容:
抛开与计算机硬件、软件相关的因素,算法的运行时间只依赖于算法的好坏和问题的输入规模,这是我们分析一个算法的关键。下面通过一个求和的例子来计算算法的时间复杂度:
int i, sum = 0, n = 100; // 执行1次 for(i = 1; i <= n ; i++) { // 执行n次 sum = sum + i; } printf("%d\n", sum); // 执行1次 |
这个程序一共运行了1 + n + 1 = n+2 次,而如果将程序改成下面这样:
int sum = 0, n = 100; // 执行1次 sum = (1 + n) * n / 2; // 执行1次 printf("%d\n", sum); // 执行1次 |
则同样是求和,这个程序只执行了 1 + 1 + 1 = 3 次,好坏显而易见。如果我们忽略开头和结尾,只关注代码的中间计算部分,则这两个算法的差别就是n次与1次的差距。
如果将上面的程序改成这样:
int i, j, sum = 0, n = 100; // 执行1次 for(i = 1; i <= 100; i++){ // 执行n^2次 for(j = 1; j <= 100; j++){ sum = sum + j; } } printf("%d\n", sum); // 执行1次 |
这个例子一共要执行n^2+2 次,显然这个算法相对于输入规模 n=100 来说,执行次数要比前两个算法很多,而且可以预测,随着n的增大,执行次数的增加也将远远多于前面两个算法。
到这里已经可以发现,测试算法运行时间最可靠的方法就是计算对运行时间有消耗的基本操作的执行次数,把这个执行次数写成与输入规模相关的函数来表示算法的复杂度,比如上面的三个复杂度函数分别为:
f1(n) = n + 3;
f2(n) = 3;
f3(n) = n^2 + 2;
在近似比较算法的时间效率时,不需要通过详细的公式去推导算法之间的差别,只需要比较算法的渐近增长就可以了,假设有以下几个算法:
在n较小时,比较几个算法的执行次数并不能判断谁好谁坏,但随着n的增大,可以明显地看到,A最好,D最差,中间的B, B', C各有差别,但是差别也不大,这就是函数渐增长性。如果输入规模n没有限制,浙进增长最慢的算法是最优的。
算法的渐进增长可以表示成大O记法,大O记法基于以下一点计算:
判断一个算法的效率,函数中的常数和其他次要项常常可以忽略,而更应该关注最高项的阶数。
推导算法的大O阶的方法是:
通过以上方法,就可以得到算法的大O阶表示的时间复杂度。
int sum = 0, n = 100; // 执行1次 sum = (1 + n) * n / 2; // 执行1次 printf("%d\n", sum); // 执行1次 |
int i, sum = 0, n = 100; // 执行1次 for(i = 1; i <= n ; i++){ // 执行n + 1次 sum = sum + i; } printf("%d\n", sum) ; // 执行1次 |
int i = 1, n = 100; // 执行1次 while(i < n) { // 执行log2(n)次 i = i * 2; } |
int i, j, sum = 0, n = 100; // 执行1次 for(i = 1; i <= 100; i++){ // 执行n^2次 for(j = 1; j <= 100; j++){ sum = sum + j; } } printf("%d\n", sum); // 执行1次 |
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < 0(n^n)
分析一个算法时,往往还需要考虑算法在最坏情况和平均情况下的运行时间。以顺序查找为例,从n个元素的数组中查找某个数字时,如果数组的第一个元素就是查找的目标,则此时算法的时间复杂度为O(1),但如果这个数字在数组的最后一个位置,则查找算法的时间复杂度将是O(n),这是最坏的情况。
最坏情况运行时间是一种保证,在应用中是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况下的运行时间。
而平均运行时间也就是从概率的角度看,这个数字在每个位置出现的可能性都是相同的,所以平均的查找时间为n/2次后发现目标元素。
对算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度称为平均时间复杂度,一种情况是统计最坏情况下的时间复杂度,称为最坏时间复杂度。一般在没有特殊说明时,都是指最坏时间复杂度。