数据结构(邓俊辉)1-2---复杂度分析

616 阅读7分钟

目的

明确了算法复杂度的度量标准后,不清楚的可点击复杂度度量 ,如何分析具体算法的复杂度? 大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 + |log2nlog_2n|(向上取整) 次循环,n必然缩减至0,从而算法终止。“ 我的理解如下: 根据右移运算的性质(非负整数右移一位,相当于/2),每右移一位,n都至少缩减一半。

∀ 正整数n(1,2,3...n),均可表示成n = kxk^x, 其中k表示右移运算的位数 k >1 ,x = logknlog_kn n右移一位,故k = 2,n可表示为2x2^x,x= log2nlog_2n n = 1 , x = log21log_21=0;
n = 2 , x = log22log_22=1; n = 3,1 < x=log23<2log_23 < 2;
|log23log_23| = 1; 所以,至多经过1 + |log2nlog_2n|(向上取整) 次循环,n必然缩减至0

从另一角度来看,1 + |log2nlog_2n|恰为n二进制展开的总位数

当n = 2时, 1 + |log22log_22| = 2 当n = 3时, 1 + |log23log_23| = 2 当n = 8时; 1 + |log28log_28| = 4 当n= 441(110111001)时; 1 + |log2441log_2441| = 1 + |8.784635| = 9

每次循环都将其右移一位,总的循环次数自然也应是1 + |log2nlog_2n|。 该循环体内只涉及常数次(逻辑判断、位与运算、加法、右移等)基本操作。因此,countOnes()算法的执行时间主要由循环的次数决定,亦即: O(1 + |log2nlog_2n|) = O(|log2nlog_2n|) = O(log2nlog_2n) 尽管此处底数为常数2,却可直接记作O(logn) ,对数复杂度与下标无关。此类算法称作具有“对数时间复杂度”。

3. 对数多项式复杂度

凡运行时间可以表示和度量为T(n) = O(logcnlog^cn)形式的这一类算法(其中常数c > 0),均统称作“对数多项式时间复杂度的算法”(polylogarithmic-time algorithm)。上述O(logn)即c = 1的特例。 此类算法的效率虽不如常数复杂度算法理想,从多项式的角度看仍能无限接近于常数,故也是极为高效的一类算法。 证明如下: 引入第一个重要结论: n的n次方根的极限为1,最大值为1

limnn1n\lim\limits_{n\rightarrow\infty}n^\frac{1}{n} =1 等价 limnln(n1n)\lim\limits_{n\rightarrow\infty}ln(n^\frac{1}{n}) =0 limnln(n1n)\lim\limits_{n\rightarrow\infty}ln(n^\frac{1}{n}) = limnln(n)n\lim\limits_{n\rightarrow\infty}\frac{ln(n)}{n} 无穷大比无穷大,洛必达法则 limnln(n)n\lim\limits_{n\rightarrow\infty}\frac{ln(n)}{n} = limn1n\lim\limits_{n\rightarrow\infty}\frac{1}{n} 当n->∞ 极限为0 所以 limnn1n\lim\limits_{n\rightarrow\infty}n^\frac{1}{n} =1

引入第二个重要结论: 对 ∀ ε\varepsilon > 0,都有 logn = O(nεn^\varepsilon)。

ε\varepsilon > 0 ,根据eεe^\varepsilon > 1,存在M >0,使得n > M之后 eεe^\varepsilon > 1 > n1nn^\frac{1}{n} 以 e为底取对数,便得 lnnn\frac{lnn}{n} < ε\varepsilon 亦即:lnn < nε\varepsilon 令N =eMe^M ,则n > N(即ln n > M)后,总有ln(ln n) < ε\varepsilonln n,ln n < nεn^\varepsilon

总结:

04.png

线性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(n2n^2),f(n) = n 即属于此类。 多项式级的运行时间成本,在实际应用中一般被认为是可接受的或可忍受的。某问题若存在一个复杂度在此范围以内的算法,则称该问题是可有效求解的或易解的(tractable)。 在这里插入图片描述

指数O(2n2^n)

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 + |log2nlog_2n|作为输入规模,则运行时间为O(2r2^r)。 一般地,凡运行时间可以表示和度量为T(n) = O(ana^n)形式的算法(a > 1),均属于“指数时间复杂度算法”。

从多项式到指数

05.png

复杂度层次

利用大O记号,不仅可以定量地把握算法复杂度的主要部分,而且可以定性地由低至高将复 杂度划分为若干层次。 在这里插入图片描述 在这里插入图片描述复杂度的典型层次:(1)~(7)依次为O(logn)、O( n\sqrt{n})、O(n)、O(nlogn)、O(n2n^2)、O(n3n^3)和O(2n2^n)

07.png

输入规模

不同的人在不同场合下关于“输入规模”的理解、定义和度量可能不尽相同,因此也可能导 致复杂度分析的结论有所差异。

  1. countOnes()算法的复杂度为O(logn)的结论,是相对于输入整数本身的数值n而言;而若以n二进制展开的宽度r = 1 + |log2nlog_2n|作为输入规模,则应为线性复杂度O( r )。
  2. power2BF_I()算法的复杂度为O(2r2^r)的结论,是相对于输入指数n的二进制数位r而言;而若以n本身的数值作为输入规模,却应为线性复杂度O(n)。

countOnes()算法和power2BF_I()算法将输入参数n二进制展开的宽度r作为输入规模更为合理。

综上,输入规模应严格定义为“用以描述输入所需的空间规模(这个需要好好理解)”。 需要笔记的朋友可留言

参考书籍《数据结构》邓俊辉编著