时间复杂度,复杂类以及对快速排序算法的分析
有学弟问到了一个关于"快排的时间复杂度的证明方法"的问题,在这里干脆将其来龙去脉讲清楚;在本文中我会先介绍一些基本概念,然后以快排算法为例,讲述如何应用这些概念来分析算法性能;
基本概念
定义(时间复杂度) 算法中基本操作重复执行的次数是问题规模 n n n的某个函数,其时间量度记作: T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n)),称作算法的渐近时间复杂度(Asymptotic Time complexity),简称时间复杂度.一般地,常用最深层循环内的语句中的原操作的执行频度(重复执行的次数)来表示.
更形式的定义可以这么表述:
定义(渐进时间复杂度(asymptotic time complectiy)): 若存在函数 f ( n ) f(n) f(n),使得当 n n n趋近于无穷大时, T ( n ) / f ( n ) T(n)/f(n) T(n)/f(n)的极限值为不等于零的常数,则称 f ( n ) f(n) f(n)是 T ( n ) T(n) T(n)的同数量级函数.记作 T ( n ) = O ( f ( n ) ) T(n)= O(f(n)) T(n)=O(f(n)),称 O ( f ( n ) ) O(f(n)) O(f(n))为算法的渐进时间复杂度,简称时间复杂度.
渐进时间复杂度用大写 O O O来表示,所以也被称为大 O O O表示法.
这就引出了算法分析的几种表示法:
- 大 O O O表示法: 算法的上紧界(tight upper bound);
- 大 Ω \Omega Ω表示法: 算法的下紧界(greatest lower bound);
- 大 Θ \Theta Θ表示法: 算法的平均运行时间(tight upper bound);
因此我们易得: Ω ( f ( n ) ) ≤ Θ ( f ( n ) ) ≤ O ( f ( n ) ) \Omega(f(n)) \le \Theta(f(n)) \le O(f(n)) Ω(f(n))≤Θ(f(n))≤O(f(n));
分治算法主定理
其实我们可以将这个定理看作一个解关于分治算法时间复杂度的递推方程的一个分情况的定理;
定理(分治算法主定理): 分治算法通常遵守一种通用模式:即在解决规模为 n n n的问题时,总是先递归地求解 a a a个规模为 n / b n/b n/b的子问题,然后在 O ( n d ) O(n^d) O(nd)时间内将子问题的解合并起来,其中 a , b , d > 0 a,b,d>0 a,b,d>0是一些特定的整数.分治算法的运行时间可以通过公式:
T ( n ) = a T ( ⌈ n b ⌉ ) + O ( n d ) T(n) = aT(\lceil \frac{n}{b} \rceil) + O(n^d) T(n)=aT(⌈bn⌉)+O(nd)
得出.以下将给出这类一般递推式的一个封闭解,以便由于遇到新的问题时,无需重新求解.如果对于常数 a > 0 a>0 a>0, b > 1 b>1 b>1以及 d ≥ 0 d \ge 0 d≥0,有 T ( n ) = a T ( ⌈ n b ⌉ ) + O ( n d ) T(n) = aT(\lceil \frac{n}{b} \rceil) + O(n^d) T(n)=aT(⌈bn⌉)+O(nd)成立,则:
T ( n ) = { O ( n d ) d > l o g b a O ( n d l o g 2 n ) d = l o g b a O ( n l o g b a ) d < l o g b a T(n) = \begin{cases} O(n^d) & d > log_ba \\ O(n^dlog_2n) & d = log_ba \\ O(n^{log_ba}) & d < log_ba \end{cases} T(n)=⎩⎪⎨⎪⎧O(nd)O(ndlog2n)O(nlogba)d>logbad=logbad<logba
example(归并排序): 归并排序的时间复杂度为 T ( n ) = 2 T ( ⌈ n 2 ⌉ ) + O ( n 1 ) T(n) = 2T(\lceil \frac{n}{2} \rceil) + O(n^1) T(n)=2T(⌈2n⌉)+O(n1),那么 a = 2 a=2 a=2, b = 2 b=2 b=2, d = 1 d=1 d=1,则 d = 1 = log b a = 1 d=1=\log_ba=1 d=1=logba=1,由主定理可知 T ( n ) = O ( n d l o g 2 n ) = O ( n l o g 2 n ) T(n) = O(n^dlog_2n) = O(nlog_2n) T(n)=O(ndlog2n)=O(nlog2n);
常见的时间复杂度
时间复杂度 | 名称 | 例如 |
---|---|---|
O ( 1 ) O(1) O(1) | 常数时间 | 链表的元素添加; |
O ( l o g n ) O(logn) O(logn) | 对数时间 | 在二叉搜索树中查找元素; |
O ( n ) O(n) O(n) | 线性时间 | 在无序数组中找特定元素; |
O ( n l o g n ) O(nlogn) O(nlogn) | 线性对数时间 | 归并排序; |
O ( n 2 ) O(n^2) O(n2) | 平方时间 | 给出图中 ∀ \forall ∀两个节点的最短路径; |
O ( n ) O(n) O(n) | 立方时间 | 矩阵乘法; |
O ( 2 n ) O(2^n) O(2n) | 指数时间 | 汉诺塔问题; |
O ( n ! ) O(n!) O(n!) | 阶乘时间 | 九连环问题; |
什么是复杂度类
这是一个初学者容易和时间复杂度混淆的概念,但是其中确实也是有联系的;在计算复杂度理论中,一个复杂度类指的是一群复杂度类似的问题的集合.一个典型的复杂度类的定义有以下形式:可以被同一个抽象机器M使用 O ( f ( n ) ) O(f(n)) O(f(n))的资源R所解决的问题的集合( n n n是输入数据的大小),复杂度类的类型有如下几种:
- P: 能在多项式时间内被确定型图灵机(deterministic machine)解决的问题.
- NP: 不能在多项式时间内解决或不确定能不能在多项式时间内解决,但能在多项式时间验证的问题.
- NPC: NP完全问题,所有NP问题在多项式时间内都能约化(Reducibility)到它的NP问题,即解决了此NPC问题,所有NP问题也都得到解决.
- NP-hard: NP难问题,所有NP问题在多项式时间内都能约化(Reducibility)到它的问题(不一定是NP问题).
对快速排序算法的分析
(basic idea): 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列.(更多快排本身的细节可见这篇blog)
现在我们来分析快速排序算法,我们假设 T ( n ) T(n) T(n)是快排算法QUICKSORT在输入规模为 n n n的数据集合上所花费的时间,那么有递归式:
T ( n ) = max 0 ≤ q ≤ n − 1 ( T ( q ) + T ( n − q − 1 ) + Θ ( n ) ) T(n) = \max_{0\le q \le n-1}(T(q)+T(n-q-1)+\Theta(n)) T(n)=0≤q≤n−1max(T(q)+T(n−q−1)+Θ(n))
在这里我们对快排算法QUICKSORT每一次的规模拆分的设计是 q : n − q − 1 q:n-q-1 q:n−q−1;
我们不妨设 T ( n ) ≤ c n 2 T(n)\le cn^2 T(n)≤cn2成立(废话…最烂烂不过插入排序对吧)则带入上式:
T ( n ) ≤ c ⋅ max 0 ≤ q ≤ n − 1 ( q 2 + ( n − q − 1 ) 2 ) + Θ ( n ) T(n) \le c \cdot \max_{0\le q \le n-1}(q^2+(n-q-1)^2) +\Theta(n) T(n)≤c⋅0≤q≤n−1max(q2+(n−q−1)2)+Θ(n)
而 q 2 + ( n − q − 1 ) 2 q^2+(n-q-1)^2 q2+(n−q−1)2在区间 0 ≤ q ≤ n − 1 0\le q \le n-1 0≤q≤n−1的端点上取最大值,由于该表达式二阶导数为正,因此可以知道当拆分是对半拆分时也是最佳的时间情形;综上所述:
- 最优情况下时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)(根据分治算法主定理);
- 最差情况下时间复杂度: O ( n 2 ) O(n^2) O(n2)(根据分治算法主定理);
- 最优的情况下空间复杂度为: O ( l o g n ) O(logn) O(logn) 每一次都平分数组的情况;
- 最差的情况下空间复杂度为: O ( n ) O(n) O(n) 退化为冒泡排序的情况;
问题解答
张:这么说,快排并不是什么"最优的排序方法"?
答曰: 是的,但是它是在现实实践中表现最佳的算法,这不是定理给出的,而是实验给出的(当我们每次以 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n⌉拆分规模时);因此你的"怎么证明它比其他任何排序都优异,以至于不需要再需要构思其他排序算法了"也就不需要回答了;
张:你讲的根本不清楚啊/讲的没有意义啊/根本没有回答我的问题啊/你不行啊…
答曰: 是的,但你仍然可以自己钻研<<算法导论>>,书就在我桌子上,我相信凭借你更加聪明的头脑可以从第七章找出答案;
张:那当然…
代码实现
#include<stdio.h>
// 打印数组
void print_array(int *array, int length)
{
int index = 0;
printf("array:\n");
for(; index < length; index++){
printf(" %d,", *(array+index));
}
printf("\n\n");
}
void quickSort(int array[], int length)
{
int start = 0;
int end = length-1;
int value = array[start];// 得到"哨兵"元素
if (1 > length) return;// 递归出口
while(start < end){// 以"哨兵"元素为标准,分成大于它和小于它的两列元素
while(start < end){// 从数组尾部往前循环得到小于"哨兵"元素的一个元素
if ( array[end--] < value ){
array[start++] = array[++end];
break;
}
}
while( start < end ){// 从数组头部往后循环得到大于"哨兵"元素的一个元素
if( array[start++] > value){
array[end--] = array[--start];
break;
}
}
}
array[start] = value;// 放置"哨兵"元素
printf("\nstart:%d, end:%d\n", start, end);// 这个是测试下start和end是否一样
quickSort(array, start);// 递归排序小于"哨兵"元素的那一列元素
quickSort(array + start + 1, length - start - 1);// 递归排序大于"哨兵"元素的那一列
}
int main(void)
{
int array[12] = {1,11,12,4,2,6,9,0,3,7,8,2};
print_array(array, 12);// 开始前打印下
quickSort(array, 12);// 快速排序
print_array(array, 12);// 排序后打印下
return 0;
}
编译及运行(实现细节可见这篇blog):
root@hanss-OptiPlex-9010:# gcc qsort.c -o a.out
root@hanss-OptiPlex-9010:# ./a.out
array:
1, 11, 12, 4, 2, 6, 9, 0, 3, 7, 8, 2,
start:1, end:1
start:0, end:0
start:9, end:9
start:0, end:0
start:2, end:2
start:1, end:1
start:0, end:0
start:3, end:3
start:2, end:2
start:0, end:0
start:0, end:0
start:0, end:0
array:
0, 1, 2, 2, 3, 4, 6, 7, 8, 9, 11, 12,