【数据结构(邓俊辉)学习笔记】绪论02——复杂度分析

目的

明确了算法复杂度的度量标准后,不清楚的可点击复杂度度量 ,如何分析具体算法的复杂度?
大O记号将各算法的复杂度分为若干层次级别,若干经典复杂度级别如下:

1. 常数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),并在编译过程中作相应的自动优化。
然而不幸的是,对于由变量(变量在程序运行时才可确定)参与定义的这种(以及更为复杂的)逻辑条件,编译器尚不能有效地判别和优化。
不难理解,相对于前两种情况,后两种无效的分支语句几乎无法有效地辨别。由以上可见,对于程序时间复杂度的估算,不能完全停留和依赖于其外在的流程结构;更为准确而精细的分析,必然需要以对其内在功能语义的充分理解为基础。

2. 对数O(logn)

问题与算法

对于任意非负整数,统计其二进制展开中数位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; //返回计数
} 

复杂度

书中原话”至多经过1 + | l o g 2 n log_2n log2n|(向上取整) 次循环,n必然缩减至0,从而算法终止。“
我的理解如下:
根据右移运算的性质(非负整数右移一位,相当于/2),每右移一位,n都至少缩减一半。

∀ 正整数n(1,2,3…n),均可表示成n = k x k^x kx, 其中k表示右移运算的位数 k >1 ,x = l o g k n log_kn logkn
n右移一位,故k = 2,n可表示为 2 x 2^x 2x,x= l o g 2 n log_2n log2n
n = 1 , x = l o g 2 1 log_21 log21=0;
n = 2 , x = l o g 2 2 log_22 log22=1;
n = 3,1 < x= l o g 2 3 < 2 log_23 < 2 log23<2;
| l o g 2 3 log_23 log23| = 1;
所以,至多经过1 + | l o g 2 n log_2n log2n|(向上取整) 次循环,n必然缩减至0

从另一角度来看,1 + | l o g 2 n log_2n log2n|恰为n二进制展开的总位数

当n = 2时, 1 + | l o g 2 2 log_22 log22| = 2
当n = 3时, 1 + | l o g 2 3 log_23 log23| = 2
当n = 8时; 1 + | l o g 2 8 log_28 log28| = 4
当n= 441(110111001)时; 1 + | l o g 2 441 log_2441 log2441| = 1 + |8.784635| = 9

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

对数多项式复杂度

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

lim ⁡ n → ∞ n 1 n \lim\limits_{n\rightarrow\infty}n^\frac{1}{n} nlimnn1 =1 等价
lim ⁡ n → ∞ l n ( n 1 n ) \lim\limits_{n\rightarrow\infty}ln(n^\frac{1}{n}) nlimln(nn1) =0
lim ⁡ n → ∞ l n ( n 1 n ) \lim\limits_{n\rightarrow\infty}ln(n^\frac{1}{n}) nlimln(nn1) = lim ⁡ n → ∞ l n ( n ) n \lim\limits_{n\rightarrow\infty}\frac{ln(n)}{n} nlimnln(n)
无穷大比无穷大,洛必达法则
lim ⁡ n → ∞ l n ( n ) n \lim\limits_{n\rightarrow\infty}\frac{ln(n)}{n} nlimnln(n) = lim ⁡ n → ∞ 1 n \lim\limits_{n\rightarrow\infty}\frac{1}{n} nlimn1 当n->∞ 极限为0
所以 lim ⁡ n → ∞ n 1 n \lim\limits_{n\rightarrow\infty}n^\frac{1}{n} nlimnn1 =1

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

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

总结:
在这里插入图片描述

3. 线性O(n)

问题与算法

计算给定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)

复杂度

O(1) + O(1)*n = O(n + 1) = O(n)
凡运行时间可以表示和度量为T(n) = O(n)形式的这一类算法,均统称作“线性时间复杂度算法”。
此类算法的效率亦足以令人满意。

4. 多项式O(polynomial(n))

若运行时间可以表示和度量为T(n) = O(f(n))的形式,而且f(x)为多项式,则对应的算法称作多项式时间复杂度算法
T(n) = O( n 2 n^2 n2),f(n) = n 即属于此类。
多项式级的运行时间成本,在实际应用中一般被认为是可接受的或可忍受的。某问题若存在一个复杂度在此范围以内的算法,则称该问题是可有效求解的或易解的(tractable)。
在这里插入图片描述

5. 指数O( 2 n 2^n 2n)

问题与算法

__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癿比特位数

复杂度

算法power2BF_I()共需O(n)时间。若以输入指数n的二进制位数r = 1 + | l o g 2 n log_2n log2n|作为输入规模,则运行时间为O( 2 r 2^r 2r)。
一般地,凡运行时间可以表示和度量为T(n) = O( a n a^n an)形式的算法(a > 1),均属于“指数时间复杂度算法”。

6. 从多项式到指数

在这里插入图片描述

7.复杂度层次

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

在这里插入图片描述

8. 输入规模

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

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

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

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

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

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值