[入门必看]数据结构1.2:算法和算法评价
第一章 绪论
小题考频:8
大题考频:11
1.2 算法和算法评价
难度:☆☆☆
知识总览
1.2_1 算法的基本概念
1.2_2 算法效率的度量
1.2_1 算法的基本概念
- 什么是算法?
程序 = 数据结构 + 算法
数据结构:如何用数据正确地描述现实世界的问题,并存入计算机
算法:如何高效地处理这些数据,以解决实际问题
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
算法 - 求解问题的步骤
Eg. 要解决的问题:做番茄炒蛋
- 食材:鸡蛋、西红柿、料酒、糖、盐。 - 数据结构
- 处理食材的步骤:切西红柿、打鸡蛋、翻炒、调味、装盘。 - 算法
Eg.算法:将该线性表按照年龄递增排序
Step1:扫描5个元素,找到年龄最小的一个元素,插入到第1个位置Step 2:扫描剩下的4个元素,找到年龄最小的一个元素,插入到第2个位置
Step 3:扫描剩下的3个元素,找到年龄最小的一个元素,插入到第3个位置
Step 4:扫描剩下的2个元素,找到年龄最小的一个元素,插入到第4个位置
这是一个算法吗?
算法的特性
算法必须具备的特征:
- 有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
注:算法必须是有穷的,而程序可以是无穷的
算法 - 用有限步骤解决某个特定的问题
程序 - 如:微信是程序,不是算法
死循环也不是算法
- 确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
Eg.
其中有两人的年龄相同,想要得到一个年龄递增的结果,上述两个排列结果都是正确的。
但是对于一个相同的输入,第一次处理后得到第一种结果,第二次处理后得到第二种结果,那么就不能称之为算法,其不具备确定性。
要求对于相同的输入,只能得出相同的输出,就是要求算法中的指令无歧义。
加入一个无歧义的明确规则:对于岁数相同的元素,原本排在前的人,会被优先考虑,那么对于相同的输入就可以得到相同的输出。
所以对于刚才给出的算法描述中,是存在歧义的,其不具备确定性,不可以称为算法。
- 可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
给出的实现方案必须能够用计算机代码来实现。
- 输入。一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
给算法处理的数据
Eg. "Hello World! - 零个输入。
Eg. 上述排序案例,处理的数据来自个人财富数据对象 - 输入来自某个特定的数据对象
- 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
类似于函数:y=f(x)
“好”算法的特质
即设计算法时要尽量追求的目标
- 正确性。算法应能够正确地解决求解问题。
是算法 - 不是“好”算法
- 可读性。算法应具有良好的可读性,以帮助人们理解。
注:算法可以用代码、伪代码描述,甚至用文字描述,重要的是要“无歧义”地描述出解决问题的步骤
代码写//注释 - 提高可读性
- 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
4. 高效率与低存储量需求
高效率 - 花的时间少。时间复杂度低
低存储量需求 - 不费内存。空间复杂度低
1.2_2 算法效率的度量
算法效率的度量是通过时间复杂度和空间复杂度来描述的。
时间复杂度
如何评估算法时间开销?
——让算法先运行,事后统计运行时间?
Q:存在什么问题?
- 和机器性能有关,如:超级计算机v.s.单片机
- 和编程语言有关,越高级的语言执行效率越低
- 和编译程序产生的机器指令质量有关
能否排除与算法本身无关的外界因素
- 有些算法是不能事后再统计的,如:导弹控制算法
能否事先估计?
算法时间复杂度:T = T(n)
事前预估算法时间开销T(n)与问题规模n的关系
注:T表示“Time”
Eg.
用算法表白——“爱你n遍”
分析问题规模n与时间执行时间t之间的关系:
语句频度:
① ——1次
② ——3001次
③④ ——3000次
⑤ ——1次
T(3000) = 1 + 3001 + 2*3000 + 1
时间开销与问题规模n的关系:
T
(
n
)
=
3
n
+
3
T(n) = 3n + 3
T(n)=3n+3
问题1:是否可以忽略表达式某些部分?
问题2:如果有好几千行代码,按这种方法需要一行一行数?
简化时间复杂度
问题1:是否可以忽略表达式某些部分 - 能否简化时间复杂度
T
1
(
n
)
=
3
n
+
3
T_1(n) = 3n + 3
T1(n)=3n+3 ->
3
n
3n
3n ->
n
n
n
T
2
(
n
)
=
n
2
+
3
n
+
1000
T_2(n) = n^2 + 3n + 1000
T2(n)=n2+3n+1000 ->
n
2
n^2
n2
T
3
(
n
)
=
n
3
+
n
2
+
9999999
T_3(n) = n^3 + n^2 + 9999999
T3(n)=n3+n2+9999999 ->
n
3
n^3
n3
当问题规模n足够大时,可以忽略掉低阶的项。
若n=3000,则
3
n
=
9000
3n = 9000
3n=9000 V.S.
T
1
(
n
)
=
9003
T_1(n)=9003
T1(n)=9003
n
2
=
9
,
000
,
000
n^2=9,000,000
n2=9,000,000 V.S.
T
2
(
n
)
=
9
,
010
,
000
T_2(n)=9,010,000
T2(n)=9,010,000
n
3
=
27
,
000
,
000
,
000
n^3=27,000,000,000
n3=27,000,000,000 V.S.
T
3
(
n
)
=
27
,
018
,
999
,
999
T_3(n)=27,018,999,999
T3(n)=27,018,999,999
- 结论1:可以只考虑阶数高的部分
T
(
n
)
=
O
(
f
(
n
)
)
⇔
lim
n
→
∞
T
(
n
)
f
(
n
)
=
k
T\left( n \right) =O\left( f\left( n \right) \right) \Leftrightarrow \underset{n\rightarrow \infty}{\lim}\frac{T\left( n \right)}{f\left( n \right)}=k
T(n)=O(f(n))⇔n→∞limf(n)T(n)=k
当n=3000时,
9999
n
=
29
,
997
,
000
9999n = 29,997,000
9999n=29,997,000 远小于
n
3
=
27
,
018
,
999
,
999
n^3=27,018,999,999
n3=27,018,999,999
当n=1000000时
9999
n
=
9
,
999
,
000
,
000
9999n=9,999,000,000
9999n=9,999,000,000 远小于
n
2
=
1
,
000
,
000
,
000
,
000
n^2=1,000,000,000,000
n2=1,000,000,000,000
- 结论2:问题规模足够大时,常数项系数也可以忽略
在分析一个程序的时间复杂性时,有以下两条规则:
-
加法规则
T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( max ( f ( n ) , g ( n ) ) ) T\left( n \right) =T_1\left( n \right) +T_2\left( n \right) =O\left( f\left( n \right) \right) +O\left( g\left( n \right) \right) =O\left( \max \left( f\left( n \right) ,g\left( n \right) \right) \right) T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))
多项相加,只保留最高阶的项,且系数变为1 -
乘法规则
T ( n ) = T 1 ( n ) × T 2 ( n ) = O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) T\left( n \right) =T_1\left( n \right) \times T_2\left( n \right) =O\left( f\left( n \right) \right) \times O\left( g\left( n \right) \right) =O\left( f\left( n \right) \times g\left( n \right) \right) T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
多项相乘,都保留
Eg.
T
3
(
n
)
=
n
3
+
n
2
l
o
g
2
n
T_3(n)=n^3+n^2log_2n
T3(n)=n3+n2log2n
=
O
(
n
3
)
+
O
(
n
2
l
o
g
2
n
)
=O(n^3)+O(n^2log_2n)
=O(n3)+O(n2log2n)
=
O
(
n
2
n
)
+
O
(
n
2
l
o
g
2
n
)
=
?
=O(n^2n)+O(n^2log_2n)=?
=O(n2n)+O(n2log2n)=?
谁的数量级更大?
常见的渐近时间复杂度为:
O
(
1
)
<
O
(
log
2
n
)
<
O
(
n
)
<
O
(
n
log
2
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O\left( 1 \right) <O\left( \log _2n \right) <O\left( n \right) <O\left( n\log _2n \right) <O\left( n^2 \right) <O\left( n^3 \right) <O\left( 2^n \right) <O\left( n! \right) <O\left( n^n \right)
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
上述例子中,哪个的阶数更高(时间复杂度更高)?
T
1
(
n
)
=
O
(
n
)
T_1(n)=O(n)
T1(n)=O(n)
T
2
(
n
)
=
O
(
l
o
g
2
n
)
T_2(n)=O(log_2n)
T2(n)=O(log2n)
lim
n
→
∞
log
2
n
n
⟹
洛必达
lim
n
→
∞
1
n
ln
2
=
0
\underset{n\rightarrow \infty}{\lim}\frac{\log _2n}{n}\overset{\text{洛必达}}{\Longrightarrow}\underset{n\rightarrow \infty}{\lim}\frac{1}{n\ln 2}=0
n→∞limnlog2n⟹洛必达n→∞limnln21=0
当
n
→
∞
n \rightarrow \infty
n→∞时,
n
n
n比
l
o
g
2
n
log_2n
log2n变大的速度快很多
lim
n
→
∞
n
2
2
n
⟹
洛必达
lim
n
→
∞
2
n
2
n
ln
2
⟹
洛必达
lim
n
→
∞
2
2
n
ln
2
2
=
0
\underset{n\rightarrow \infty}{\lim}\frac{n^2}{2^n}\overset{\text{洛必达}}{\Longrightarrow}\underset{n\rightarrow \infty}{\lim}\frac{2n}{2^n\ln 2}\overset{\text{洛必达}}{\Longrightarrow}\underset{n\rightarrow \infty}{\lim}\frac{2}{2^n\ln ^22}=0
n→∞lim2nn2⟹洛必达n→∞lim2nln22n⟹洛必达n→∞lim2nln222=0
当
n
→
∞
n \rightarrow \infty
n→∞时,
2
n
2^n
2n比
n
2
n^2
n2变大的速度快很多
咒语:常对幂指阶
问题1:是否可以忽略表达式某些部分 - 能否简化时间复杂度
答:只考虑阶数,用大O记法表示
考虑最深层循环与n的关系
问题2:如果有好几千行代码,按这种方法需要一行一行数?
Eg1.
- 结论1:顺序执行的代码只会影响常数项,可以忽略
- 结论2:只需挑循环中的一个基本操作分析它的执行次数与n的关系即可
Eg2.
- 结论3:如果有多层嵌套循环,只需关注最深层循环循环了几次
问题2:如果有好几千行代码,按这种方法需要一行一行数?
答:只需考虑最深层循环的循环次数与n的关系
练习1.
计算上述算法的时间复杂度T(n):
设最深层循环的语句频度(总共循环的次数)为x,则
i
=
2
,
4
,
8
,
16
,
32
,
…
i=2,4,8,16,32,…
i=2,4,8,16,32,…
由循环条件可知,循环结束时刚好满足
i
=
2
x
>
n
i=2^x > n
i=2x>n
此时
x
=
l
o
g
2
n
+
1
x = log_2n + 1
x=log2n+1
T
(
n
)
=
O
(
x
)
=
O
(
l
o
g
2
n
)
T(n) = O(x) = O(log2n)
T(n)=O(x)=O(log2n)
练习2.
计算上述算法的时间复杂度T(n):
最好情况:元素n在第一个位置
——最好时间复杂度
T
(
n
)
=
O
(
1
)
T(n)=O(1)
T(n)=O(1)
最坏情况:元素n在最后一个位置
——最坏时间复杂度
T
(
n
)
=
O
(
n
)
T(n)=O(n)
T(n)=O(n)
平均情况:假设元素n在任意一个位置的概率相同为
1
n
\frac{1}{n}
n1
——平均时间复杂度
T
(
n
)
=
O
(
n
)
T(n)=O(n)
T(n)=O(n)
循环次数
x
=
(
1
+
2
+
3
+
⋯
+
n
)
1
n
=
(
n
(
1
+
n
)
2
)
1
n
=
1
+
n
2
x=\left( 1+2+3+\cdots +n \right) \frac{1}{n}=\left( \frac{n\left( 1+n \right)}{2} \right) \frac{1}{n}=\frac{1+n}{2}
x=(1+2+3+⋯+n)n1=(2n(1+n))n1=21+n
T
(
n
)
=
O
(
x
)
=
O
(
n
)
T(n)=O(x)=O(n)
T(n)=O(x)=O(n)
很多算法执行时间与输入的数据有关
- 最坏时间复杂度:最坏情况下算法的时间复杂度
- 平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间
- 最好时间复杂度:最好情况下算法的时间复杂度(意义不大)
空间复杂度
程序运行时的内存需求
Eg1.
假设代码占100字节,int型变量 - 4字节;
那么局部变量i和参数n,至少还需要8字节空间。
即假设算法为108字节。
该问题中n为问题规模,但无论问题规模怎么变,算法运行所需的内存空间都是固定的常量 -(108字节)
算法空间复杂度为
S
(
n
)
=
O
(
1
)
S(n)=O(1)
S(n)=O(1)
注:S表示“Space”
- 空间复杂度为常数阶 - 算法所需内存空间为常量
——算法原地工作
Eg2.
设置数组 - 数组长度为n - 即问题规模,
所以所需的内存空间大小就和问题规模n有关系。
假设一个int变量占4字节
存放参数n占4字节 - 数组有n个元素占4n字节 - 变量i占4字节
则所需内存空间 = 4 + 4n + 4 = 4n+8 - 与问题规模n有关系
S
(
n
)
=
O
(
4
n
+
8
)
S(n)=O(4n+8)
S(n)=O(4n+8)
只关心耗费存储空间的数量级 - 阶数
S
(
n
)
=
O
(
n
)
S(n)=O(n)
S(n)=O(n)
变量和问题规模n没有关系,只会增加常数项,对最终结果没有影响。
- 所以分析算法的空间复杂度时,只需关注存储空间大小与问题规模相关的变量。
Eg3.
考虑与问题规模n相关的flag数组 - 4nn
——
4
⋅
n
2
4·n^2
4⋅n2
S ( n ) = O ( n 2 ) S(n)=O(n^2) S(n)=O(n2)
Eg4.
S
(
n
)
=
O
(
n
2
)
+
O
(
n
)
+
O
(
1
)
=
O
(
n
2
)
S(n)=O(n^2)+O(n)+O(1)=O(n^2)
S(n)=O(n2)+O(n)+O(1)=O(n2)
a) 加法规则
T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O( T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O( m a x max max ( f ( n ) , g ( n ) ) ) (f(n),g(n))) (f(n),g(n)))
<color = red> O ( 1 ) < O ( log 2 n ) < O ( n ) < O ( n log 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O\left( 1 \right) <O\left( \log _2n \right) <O\left( n \right) <O\left( n\log _2n \right) <O\left( n^2 \right) <O\left( n^3 \right) <O\left( 2^n \right) <O\left( n! \right) <O\left( n^n \right) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
函数递归调用带来的内存开销
Eg5.
其中包含函数调用自身 - 递归调用
第一层调用中,n=5,a,b,c
第二层调用中,n=4,a,b,c
——n,a,b,c名字相同,但是其中n和a,b,c在内存中都存放在与第一层调用不同的空间内
……
每一层调用都需要把这次调用中使用的n,a,b,c用一个专门的内存空间存储
函数调用栈
假设每一层函数调用都需要k字节(k为常数)
当问题规模为n时,需要的空间为kn字节。
该程序的空间复杂度为:
S
(
n
)
=
O
(
n
)
S(n)=O(n)
S(n)=O(n)
- 空间复杂度=递归调用的深度
Eg6.
每一层递归都会定义一个int类型的数组flag[n]
数组的长度与这一层递归的n相同
第一层调用中,参数n为5,数组为flag[5]…
第二层调用中,参数n为4,数组为flag[4]…
……
第五层调用中,参数n为1,数组为flag[1]…
n层递归调用情况下,所需要的存储flag数组的空间大小为:
1
+
2
+
3
+
…
+
n
=
[
n
(
1
+
n
)
]
/
2
=
1
2
n
2
+
1
2
n
1+2+3+…+n=[n(1+n)]/2=\frac{1}{2}n^2+\frac{1}{2}n
1+2+3+…+n=[n(1+n)]/2=21n2+21n
S
(
n
)
=
O
(
n
2
)
S(n)=O(n^2)
S(n)=O(n2)
知识回顾与重要考点
1.2_1 算法的基本概念
程序设计 - 设计一个好的数据结构 + 设计一个好的算法。
算法的五个特性 - 是算法必须具备的特性,其一不满足,即不能称为算法。
“好”算法的特质 - 设计算法时要尽量追求的目标。
1.2_2 算法效率的度量
时间复杂度
如何计算:基本操作 - 最深层循环 - 与n的关系
两个规则:加法规则 - 保留最高项 - 系数为1;
乘法规则,都保留。
咒语:“常对幂指阶”
主要关注 - 最坏时间复杂度&平均时间复杂度
注:算法的性能问题只有在n很大时才会暴露出来。
空间复杂度
- 普通程序 - 与n相关的变量 - 所占空间与n的关系 - 数量级O(x)为S(n)
- 递归程序 - 递归深度与n的关系 - O(x)
- 算空间复杂度O(x)时的技巧与时间复杂度相同 - 加/乘法规则&“常对幂指阶”