目的
明确了算法复杂度的度量标准后,不清楚的可点击复杂度度量 ,如何分析具体算法的复杂度? 大O记号将各算法的复杂度分为若干层次级别,若干经典复杂度级别如下:
常数O(1)
这个比较简单,简单说说。 运行时间可表示和度量为T(n) = O(1)的这一类算法,统称作“常数时间复杂度算法”。 这类算法效率最高。 此类算法通常不含循环、分支、子程序调用等,但也不能仅凭语法结构的表面形式一概而论。
void O1( unsigned int n ) {
for ( unsigned int i = 0; i < n; i += 1 + n/2013 ) { //循环:但迭代至多2013次,与n无关
UNREACHABLE: //无法抵达癿转向标志
if ( 1 + 1 != 2 ) goto UNREACHABLE; //分支:条件永非,转向无效
if ( n * n < 0 ) doSomething(n); //分支:条件永非,调用无效
if ( (n + i) * (n + i) < 4 * n * i ) doSomething( n ); //分支:条件永非,调用无效
if ( 2 == (n * n) % 5 ) O1( n + 1 ); //分支:条件永非,递归无效
}
}
目前某些高级的编译器,像是C++语言,已经能够识别前一类完全由常数定义的永非式,因为常数在编译器便可确定(可了解下constexpr),并在编译过程中作相应的自动优化。 然而不幸的是,对于由变量(变量在程序运行时才可确定)参与定义的这种(以及更为复杂的)逻辑条件,编译器尚不能有效地判别和优化。 不难理解,相对于前两种情况,后两种无效的分支语句几乎无法有效地辨别。由以上可见,对于程序时间复杂度的估算,不能完全停留和依赖于其外在的流程结构;更为准确而精细的分析,必然需要以对其内在功能语义的充分理解为基础。
对数O(logn)
1. 问题与算法
对于任意非负整数,统计其二进制展开中数位1的总数。
int countOnes ( unsigned int n ) { //统计整数二迕刢展开中数位1癿总数:O(logn)
int ones = 0; //计数器复位
while ( 0 < n ) { //在n缩减至0之前,反复地
ones += ( 1 & n ); //检查最低位,若为1则计数
n >>= 1; //右移一位
}
return ones; //返回计数
}
2.复杂度
书中原话”至多经过1 + ||(向上取整) 次循环,n必然缩减至0,从而算法终止。“ 我的理解如下: 根据右移运算的性质(非负整数右移一位,相当于/2),每右移一位,n都至少缩减一半。
∀ 正整数n(1,2,3...n),均可表示成n = , 其中k表示右移运算的位数 k >1 ,x = n右移一位,故k = 2,n可表示为,x= n = 1 , x = =0;
n = 2 , x = =1; n = 3,1 < x=;
|| = 1; 所以,至多经过1 + ||(向上取整) 次循环,n必然缩减至0
从另一角度来看,1 + ||恰为n二进制展开的总位数
当n = 2时, 1 + || = 2 当n = 3时, 1 + || = 2 当n = 8时; 1 + || = 4 当n= 441(110111001)时; 1 + || = 1 + |8.784635| = 9
每次循环都将其右移一位,总的循环次数自然也应是1 + ||。 该循环体内只涉及常数次(逻辑判断、位与运算、加法、右移等)基本操作。因此,countOnes()算法的执行时间主要由循环的次数决定,亦即: O(1 + ||) = O(||) = O() 尽管此处底数为常数2,却可直接记作O(logn) ,对数复杂度与下标无关。此类算法称作具有“对数时间复杂度”。
3. 对数多项式复杂度
凡运行时间可以表示和度量为T(n) = O()形式的这一类算法(其中常数c > 0),均统称作“对数多项式时间复杂度的算法”(polylogarithmic-time algorithm)。上述O(logn)即c = 1的特例。 此类算法的效率虽不如常数复杂度算法理想,从多项式的角度看仍能无限接近于常数,故也是极为高效的一类算法。 证明如下: 引入第一个重要结论: n的n次方根的极限为1,最大值为1。
=1 等价 =0 = 无穷大比无穷大,洛必达法则 = 当n->∞ 极限为0 所以 =1
引入第二个重要结论: 对 ∀ > 0,都有 logn = O()。
∀ > 0 ,根据 > 1,存在M >0,使得n > M之后 > 1 > 以 e为底取对数,便得 < 亦即:lnn < n 令N = ,则n > N(即ln n > M)后,总有ln(ln n) < ln n,ln n <
总结:
线性O(n)
1.问题与算法
计算给定n个整数的总和
int sumI ( int A[], int n ) { //数组求和算法(迭代版)
int sum = 0; //初始化累计器,O(1)
for ( int i = 0; i < n; i++ ) //对全部共O(n)个元素,逐一
sum += A[i]; //累计,O(1)
return sum; //迒回累计值,O(1)
} //O(1) + O(n)*O(1) + O(1) = O(n+2) = O(n)
2. 复杂度
O(1) + O(1)*n = O(n + 1) = O(n) 凡运行时间可以表示和度量为T(n) = O(n)形式的这一类算法,均统称作“线性时间复杂度算法”。 此类算法的效率亦足以令人满意。
多项式O(polynomial(n))
若运行时间可以表示和度量为T(n) = O(f(n))的形式,而且f(x)为多项式,则对应的算法称作多项式时间复杂度算法。
T(n) = O(),f(n) = n 即属于此类。
多项式级的运行时间成本,在实际应用中一般被认为是可接受的或可忍受的。某问题若存在一个复杂度在此范围以内的算法,则称该问题是可有效求解的或易解的(tractable)。

指数O()
1. 问题与算法
__int64 power2BF_I ( int n ) { //幂函数2^n算法(蛮力迭代版),n >= 0
__int64 pow = 1; //O(1):累积器刜始化为2^0
while ( 0 < n -- ) //O(n):迭代n轮,每轮都
pow <<= 1; //O(1):将累积器翻倍
return pow; //O(1):迒回累积器
} //O(n) = O(2^r),r为输入指数n癿比特位数
2.复杂度
算法power2BF_I()共需O(n)时间。若以输入指数n的二进制位数r = 1 + ||作为输入规模,则运行时间为O()。 一般地,凡运行时间可以表示和度量为T(n) = O()形式的算法(a > 1),均属于“指数时间复杂度算法”。
从多项式到指数
复杂度层次
利用大O记号,不仅可以定量地把握算法复杂度的主要部分,而且可以定性地由低至高将复
杂度划分为若干层次。
复杂度的典型层次:(1)~(7)依次为O(logn)、O( )、O(n)、O(nlogn)、O()、O()和O()
输入规模
不同的人在不同场合下关于“输入规模”的理解、定义和度量可能不尽相同,因此也可能导 致复杂度分析的结论有所差异。
- countOnes()算法的复杂度为O(logn)的结论,是相对于输入整数本身的数值n而言;而若以n二进制展开的宽度r = 1 + ||作为输入规模,则应为线性复杂度O( r )。
- power2BF_I()算法的复杂度为O()的结论,是相对于输入指数n的二进制数位r而言;而若以n本身的数值作为输入规模,却应为线性复杂度O(n)。
countOnes()算法和power2BF_I()算法将输入参数n二进制展开的宽度r作为输入规模更为合理。
综上,输入规模应严格定义为“用以描述输入所需的空间规模(这个需要好好理解)”。 需要笔记的朋友可留言
参考书籍《数据结构》邓俊辉编著