14天阅读挑战赛
努力是为了不平庸~
算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法!欢迎记录下你的那些努力时刻(算法学习知识点/算法题解/遇到的算法bug/等等),在分享的同时加深对于算法的理解,同时吸收他人的奇思妙想,一起见证技术er的成长~
一、算法知识点
1.定义
算法是对特定问题求解步骤的一种描述,它不依赖于任何一种语言,既可以用自然语言、程序设计语言描述,也可以用流程图、框图来表示。通常情况下,为了更清楚地说明算法的本质,我们会去除计算机语言的语法规则和细节,采用“伪代码”来描述算法。
2.算法的特性
(1)有穷性:算法是由若干条指令组成的有穷序列,总是在执行若干次后结束,不可能永不停止。
(2)确定性:每条语句都有确定的含义,无歧义。
(3)可行性:算法在当前环境条件下可以通过有限次运算来实现。
(4)输入/输出:有零个或多个输入以及一个或多个输出。
3.“好”算法的标准
(1)正确性:正确性是指算法能够满足具体问题的需求,程序运行正常,无语法错误,能够通过典型的软件测试,达到预期。
(2)易读性:算法遵循标识符命名规则,简洁易懂,注释语句恰当适量,方便自己和他人阅读,便于后期调试和修改。
(3)健壮性:算法对非法数据及操作有较好的反应和处理。
(4)高效性:高效性是指算法运行效率高,即算法运行所消耗的时间短。
(5)低存储性:低存储性是指算法所需的存储空间小。对于像手机、平板电脑这样的嵌入式设备,算法如果占用空间过大,则无法运行。
二、时间复杂度:算法运行所需要的时间
//算法1
int sum = 0; //运行1次
int total = 0; //运行1次
for(int i = 1;i <= n;i++){ //运行n+1次
sum = sum + i; //运行n次
for(j = 1;j <= n; j++) //运行n×(n+1)次
total = total + i*j; //运行n×n次
}
算法1所有语句的运行次数相加起来:1+1+n+1+n+n×(n+1)+n×n。用函数T(n)表示为:
T
(
n
)
=
2
n
2
+
3
n
+
3
T(n)=2n^2+3n+3
T(n)=2n2+3n+3
算法的运行时间主要取决于次数最高项,次数低项和常数可以忽略不计。
用极限可以表示为:
lim
n
→
∞
T
(
n
)
f
(
n
)
=
C
≠
0
,
C
为不等于
0
的常数
\lim_{n \to \infty} \frac{T(n)}{f(n)} =C≠0,C为不等于0的常数
n→∞limf(n)T(n)=C=0,C为不等于0的常数
T
(
n
)
T(n)
T(n)和
C
f
(
n
)
Cf(n)
Cf(n)的函数曲线如图1所示。从图1可以看出,当
n
≥
n
0
n≥n_0
n≥n0时,
T
(
n
)
≤
C
f
(
n
)
T(n)≤Cf(n)
T(n)≤Cf(n); 当n足够大时,
T
(
n
)
T(n)
T(n)和
f
(
n
)
f(n)
f(n)近似相等。因此,我们用
O
(
f
(
n
)
O(f(n)
O(f(n)表示时间复杂度渐近上界,可以用这种表示法衡量算法的时间复杂度。
渐近下界符号 Ω ( T ( n ) ≥ C f ( n ) ) Ω(T(n)≥Cf(n)) Ω(T(n)≥Cf(n)),如图2所示。从图2可以看出, 当 n ≥ n 0 n≥n_0 n≥n0时, T ( n ) ≥ C f ( n ) T(n)≥Cf(n) T(n)≥Cf(n); 当n足够大时, T ( n ) T(n) T(n)和 f ( n ) f(n) f(n)近似相等。因此,我们用 Ω ( f ( n ) ) Ω(f(n)) Ω(f(n))来表示时间复杂度渐近下界。
渐近精确界符号
Θ
(
C
1
f
(
n
)
≤
T
(
n
)
≤
C
2
f
(
n
)
)
\Theta (C_1f (n)≤T(n)≤C_2f(n))
Θ(C1f(n)≤T(n)≤C2f(n)),如图3所示。从图3可以看出,当
n
≥
n
0
n≥n_0
n≥n0时,
C
1
f
(
n
)
≤
T
(
n
)
≤
C
2
f
(
n
)
C_1f(n)≤T(n)≤C_2f(n)
C1f(n)≤T(n)≤C2f(n);当n足够大时,
T
(
n
)
T(n)
T(n)和
f
(
n
)
f(n)
f(n)近似相等。这种两边逼近的方式更加精确近似,因此我们用
Θ
(
f
(
n
)
)
\Theta (f(n))
Θ(f(n))来表示时间复杂度渐近精确界。
在实际应用中,通常使用时间复杂度渐近上界 O ( f ( n ) ) O(f(n)) O(f(n))来表示时间复杂度。
在算法分析中,渐近复杂度是对算法运行次数的粗略估计,大致反映问题规模增长趋势,而不必精确计算算法的运行时间。在计算渐近时间复杂度时,可以只考虑对算法运行时间贡献大的语句,而忽略那些运行次数少的语句。循环语句中处在循环内层的语句往往运行次数最多,它们是对运行时间贡献最大的语句。
注意:不是所有算法都能直接计算运行次数。
有些算法,如排序、查找、插入算法等,可以分为最好、最坏和平均情况分别求算法渐近复杂度。但考查个算法时通常考查最坏的情况,而不是考查最好的情况,最坏情况对衡量算法的好坏具有实际意义。
常见的算法时间复杂度有以下几类。
(1)常数阶。常数阶算法的运行次数是一个常数,通常用
O
(
1
)
O(1)
O(1)表示。
(2)多项式阶。很多算法的时间复杂度是多项式,通常用
O
(
n
)
O(n)
O(n)、
O
(
n
2
)
O(n^2)
O(n2)、
O
(
n
3
)
O(n^3)
O(n3)等表示。
(3)指数阶。指数阶算法的运行效率极差,通常用
O
(
2
n
)
O(2^n)
O(2n)、
O
(
n
!
)
O(n!)
O(n!)、
O
(
n
n
)
O(n^n)
O(nn)等表示。
(4)对数阶。对数阶算法的运行效率较高,通常用
O
(
l
o
g
n
)
O(logn)
O(logn)、
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)等表示。
指数阶增量随着
x
x
x的增加而急剧增加,而对数阶增长缓慢。它们之间的关系如下:
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
l
o
g
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
0
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1)< O(logn)< O(n)< O(nlogn) < O(n^2)< O(n^3)< 0(2^n) < O(n!)< O(n^n)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<0(2n)<O(n!)<O(nn)
在设计算法时,要注意算法复杂度增量的问题,尽量避免爆炸级增量。
三、空间复杂度:算法占用的空间大小
空间复杂度的本意是指算法在运行过程中占用了多少存储空间。算法占用的存储空间包括:
(1)输入/输出数据;
(2)算法本身;
(3)额外需要的辅助空间。
输入/输出数据占用的空间是必需的,算法本身占用的空间可以通过精简算法来缩减,但缩减的量是很小的,可以忽略不计。算法在运行时所使用的辅助变量占用的空间(即辅助空间)才是衡量算法空间复杂度的关键因素。
//算法2 交换x与y
void swap(int x, int y){
int temp;
temp = x;
x = y;
y = temp;
}
算法2使用了辅助空间temp,空间复杂度为O(1)。
在递归算法中,每一次递推都需要一个栈空间来保存调用记录, 因此在分析算法的空间复杂度时,需要计算递归栈的辅助空间。
//算法3 计算n的阶乘
int fac(int n){
if(n == 0||n == 1)
return 1;
else
return n*fac(n-1);
}
阶乘是典型的递归调用问题,递归包括递推和回归。递推是将原问题不断分解成子问题,直至满足结束条件,返回最近子问题的解;然后逆向逐一回归,最终到达递推开始的原问题,返回原问题的解。
以n=5为例,看一下计算机内部的处理过程,采用“栈”数据结构(特点:后进先出)
从图4和图5所示的进栈、出栈过程中可以很清晰地看到,子问题先被一步步地压进栈,直至直接可解得到返回值,再一步步地出栈,最终得到递归结果。在运算过程中,由于使用了n个栈空间作为辅助空间,因此阶乘递归算法的空间复杂度为O(n)。
算法3的时间复杂度也为O(n),因为n的阶乘仅比n-1的阶乘多了一次乘法运算 ( f a c ( n ) = n ∗ f a c ( n − 1 ) ) (fac(n)=n*fac(n-1)) (fac(n)=n∗fac(n−1))。如果用T(n)表示fac(n)的时间复杂度,则可以表示为 T ( n ) = T ( n − 1 ) + 1 = T ( n − 2 ) + 1 + 1 = T ( 1 ) + . . . + 1 + 1 = n T(n)= T(n-1)+1= T(n-2)+1+1= T(1)+...+1+1=n T(n)=T(n−1)+1=T(n−2)+1+1=T(1)+...+1+1=n。