一、浅谈算法
学习软件开发这么多年,常常听到程序=数据结构+算法,但是很多人对这句话提出质疑,因为实际项目开发的时候大部分人是做螺丝钉的角色,而且大部分甘于做螺丝钉的角色,就会认为实际项目,只是完成业务开发而已,去哪都是增删改查,数据结构根本用不到。我认为,算法和基本的数据结构是非常重要的,对于一个合格的程序猿来说,有时候我们没有涉及到,只是别人把需要的事情都给我们做了,比如的java版本的hashmap,采用红黑树的结构,提高了更多效率,软件开发高速发展的同时,编程的门槛也会越来越低,只有了解了最本质的才会不被技术淘汰。
算法的五大特性:
1.有穷性:不是数学,算法比较合理,每一步在规定时间内进行
2.确定性:每一条指令都有一个明确的含义
3.可行性:算法可以执行
4.输入0或者多个
5.输出 只有一个
算法设计的四大要求:
1.正确性
2.可读性
3.健壮性:容错能力,输入数据非法的时候,不会产生的输出结果
边界问题 (数组的长度的判断,非法字段,树Root是否为空)
4.效率和存储
注:1.研究算法的复杂度,侧重的是研究算法随着输入规模扩大增长量的一个抽象,而不是精确定位执行多少次
2. 不关心编译语言,不关心机器
所以我们应该用什么方式进行算法的度量方式呢?接下来我们聊聊时间复杂度
二、时间复杂度
1.概述
我们知道程序的效率可以称之为程序的时间复杂度,通俗点说就是算法执行的时间,所以将算法中基本操作的执行次数作为算法时间复杂度的度量。
比如:如何求1+2+… n的结果
第一种:O(n)
int sum=0;
for(int i=0;i<=n;i++){
sum=sum+i;
}
第二种:O(1)
int i=0;
int sum=0;
sum =(1+n)*n/2;
上述的例子可以说明如果不同的策略对待同一个需求而已,时间复杂度是不一样的,算法的优化,时间复杂度越低也是算法优化的目的之一。
**时间复杂度:**算法中基本语句重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作: T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))表示随着n的增大,算法执行的时间的增长率和f(n)的增长率相同,称渐近时间复杂度。
**函数的渐进增长:**给定两个函数,f(n).g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐进快于g(n)
上面讨论的时间复杂度是官方解释,仔细可以看时间复杂度可以表示渐进函数的抽象形式即可。
2.时间复杂度的记法:
1.大O记号 (常用)
假设 f ( n ) 和 g ( n ) f(n)和g(n) f(n)和g(n)的定义域是非负整数,存在两个正整数c和n0,使得n>n0的时候, f ( n ) ≤ c ∗ g ( n ) f(n)≤c*g(n) f(n)≤c∗g(n),则 f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n))。可见 O ( g ( n ) ) O(g(n)) O(g(n))可以表示算法运行时间的上界。 O ( g ( n ) ) O(g(n)) O(g(n))表示的函数集合的函数是阶数不超过 g ( n ) g(n) g(n)的函数。
例如: f ( n ) = 2 ∗ n + 2 = O ( n ) f(n)=2*n+2=O(n) f(n)=2∗n+2=O(n)
证明:
当
n
>
3
的
时
候
,
2
∗
n
+
2
<
3
n
,
所
以
可
选
n
0
=
3
,
c
=
3
,
则
n
>
n
0
的
时
候
,
f
(
n
)
<
c
∗
(
n
)
,
所
以
f
(
n
)
=
O
(
n
)
。
当n>3的时候,2*n +2<3n,所以可选n0=3,c=3,则n>n0的时候,f(n)<c*(n),所以f(n)=O(n)。
当n>3的时候,2∗n+2<3n,所以可选n0=3,c=3,则n>n0的时候,f(n)<c∗(n),所以f(n)=O(n)。
现在再证明 f ( n ) = 2 ∗ n + 2 = O ( n 2 ) f(n)=2*n+2=O(n^2) f(n)=2∗n+2=O(n2)
证明: 当 n > 2 的 时 候 , 2 ∗ n + 2 < 2 ∗ n 2 , 所 以 可 选 n 0 = 2 , c = 2 , 则 n > n 0 的 时 候 , f ( n ) < c ∗ ( n 2 ) , 所 以 f ( n ) = O ( n 2 ) 。 当n>2的时候,2*n+2<2*n^2,所以可选n0=2,c=2,则n>n0的时候,f(n)<c*(n^2),所以f(n)=O(n^2)。 当n>2的时候,2∗n+2<2∗n2,所以可选n0=2,c=2,则n>n0的时候,f(n)<c∗(n2),所以f(n)=O(n2)。
同理可证 f ( n ) = O ( n a ) f(n)=O(n^a) f(n)=O(na),a>1
2.Ω记号
f
(
n
)
>
c
∗
g
(
n
)
f(n) > c*g(n)
f(n)>c∗g(n)
Ω记号与大O记号相反,他可以表示算法运行时间的下界。
Ω
(
g
(
n
)
)
Ω(g(n))
Ω(g(n))表示的函数集合的函数是所有阶数超过g(n)的函数。
例如: f ( n ) = 2 ∗ n 2 + 3 ∗ n + 2 = Ω ( n 2 ) f(n)=2*n^2+3*n+2=Ω(n^2) f(n)=2∗n2+3∗n+2=Ω(n2)
证明: 当 n > 4 的 时 候 , 2 ∗ n 2 + 3 ∗ n + 2 > n 2 , 所 以 可 选 n 0 = 4 , c = 1 , 则 n > n 0 的 时 候 , f ( n ) > c ∗ ( n 2 ) , 所 以 f ( n ) = Ω ( n 2 ) 。 当n>4的时候,2*n^2+3*n+2>n^2,所以可选n0=4,c=1,则n>n0的时候,f(n)>c*(n^2),所以f(n)=Ω(n^2)。 当n>4的时候,2∗n2+3∗n+2>n2,所以可选n0=4,c=1,则n>n0的时候,f(n)>c∗(n2),所以f(n)=Ω(n2)。
同理可证 f ( n ) = Ω ( n ) , f ( n ) = Ω ( 1 ) f(n)=Ω(n),f(n)=Ω(1) f(n)=Ω(n),f(n)=Ω(1)
3.Θ记号
Θ记号介于大O记号和Ω记号之间。他表示,存在正常数c1,c2,n0,当n>n0的时候, c 1 ∗ g ( n ) ≤ f ( n ) ≤ c 2 ∗ g ( n ) c1*g(n)≤f(n)≤c2*g(n) c1∗g(n)≤f(n)≤c2∗g(n),则f ( n ) = Θ ( g ( n ) ) (n)=Θ(g(n)) (n)=Θ(g(n))。他表示所有阶数与g(n)相同的函数集合。
4.小o记号
f ( n ) = o ( g ( n ) ) 当 且 仅 当 f ( n ) = O ( g ( n ) ) 且 f ( n ) ≠ Ω ( g ( n ) ) f(n)=o(g(n))当且仅当f(n)=O(g(n))且f(n)≠Ω(g(n)) f(n)=o(g(n))当且仅当f(n)=O(g(n))且f(n)̸=Ω(g(n))。也就是说小o记号可以表示时间复杂度的上界,但是一定不等于下界。
5.例子
假设f(n)=2n^2+3n+5,
则f(n)=O(n^2)或者f(n) = O(n3)或者f(n)=O(n4)或者……
f(n)=Ω(n^2)或者f(n)=Ω(n)或者f(n)=Ω(1)
f(n)=Θ(n^2)
f(n) = o(n3)或者f(n)=o(n4)或者f(n)=o(n^5)或者……
3.时间复杂度类型
1.常数阶
如上面的例子可以知道,执行次数是常数,可以定为O(1)
int i=0;
int sum=0;
sum =(1+n)*n/2;
2.线性阶
如上述的例子可以知道,单次循环n,定为O(n)
int sum=0;
for(int i=0;i<=n;i++){
sum=sum+i;
}
3.对数阶
下面代码就表示是 O ( l o g n ) O(logn) O(logn)
while (left <= right) {
int mid = (left - right) / 2 + right;
if (target == nums[mid]) {
return mid;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
4.函数调用
main方法调用外部方法,两个方法都是一层循环,则 O ( n 2 ) O(n^2) O(n2)
int main(int argc, char *argv[])
{
for(int i=0;i<n;i++){
fun(n);
}
}
void fun(int count){
for(int i=0;i<count;i++){
printf();
}
}
常见时间复杂度的比较:
$ O(1)<O(logn)<O(n)<O(nlogn)<O(n2)…<O(n!)<O(nn)$
4. 时间复杂度的计算
1.计算规则
1) 加法规则
$T(n,m) = T1(n) + T2(n) = O ( max (f(n), g(m) ) $
- 乘法规则
$T(n,m) = T1(n) * T2(m) = O (f(n) * g(m)) $
O
(
n
)
∗
O
(
m
)
=
O
(
n
∗
m
)
O(n)*O(m)=O(n*m)
O(n)∗O(m)=O(n∗m)
3)一个特例
在大O表示法里面有 T ( n ) = T 1 ( n ) ∗ T 2 ( n ) = O ( c ∗ f ( n ) ) = O ( f ( n ) ) T(n) = T1(n) * T2(n) = O ( c*f(n) ) = O( f(n) ) T(n)=T1(n)∗T2(n)=O(c∗f(n))=O(f(n)). 一个特例,如果 T 1 ( n ) = O ( c f ( n ) ) T1(n) = O(cf(n)) T1(n)=O(cf(n)), c是一个与n无关的任意常数,$T2(n) = O ( f(n) ) $则有
总结:
1.用常数1取代所有的加法常数 t(n)=5 O(1)
2.修改后的函数中,只保留最高阶数
3.如果最高阶数的常数部分存在不是1,变成1。
比如:
T
(
n
)
=
n
3
+
n
2
+
29
,
此
时
时
间
复
杂
度
为
O
(
n
3
)
。
T
(
n
)
=
3
n
3
,
此
时
时
间
复
杂
度
为
O
(
n
3
)
T(n) = n^3 + n^2 + 29,此时时间复杂度为 O(n^3)。 T(n) = 3n^3,此时时间复杂度为 O(n^3)
T(n)=n3+n2+29,此时时间复杂度为O(n3)。T(n)=3n3,此时时间复杂度为O(n3)。
2.主定理
在算法分析中,主定理(英语:master theorem)提供了用渐近符号(大O符号)表示许多由分治法得到的递推关系式的方法。这种方法最初由Jon Bentlery,Dorothea Haken和James B. Saxe在1980年提出,在那里被描述为解决这种递推的“天下无敌法”(master method)。此方法经由经典算法教科书Cormen,Leiserson,Rivest和Stein的《算法导论》 (introduction to algorithm) 推广而为人熟知
解释: 上面的主定理就是根据递归式,我们需要找到它的时间复杂度,这里为了不区别其他的表示法,全部记为大O表示法,
例子1:
假设问题规模为N,某一个递归算法的时间程度记T(N),已知T(1) = 0,T(N) = T(N/2) + N,求用O表示该算法的时间复杂度?
分析:直接套用公式可知,a = 1, b = 2 ,f(n) = N , 主定理主要和
n
log
b
a
n^{\log_b a}
nlogba做比较,带入可得
n
log
b
a
=
1
n^{\log_b a}= 1
nlogba=1 。
所以f(n)>
n
log
b
a
n^{\log_b a}
nlogba ,符合条件三,所以T(n) = O(n)。
例子2:
假设问题规模为N,某一个递归算法的时间程度记T(N),已知T(1) = 0,T(N) = 2T(N/2) + N/2,求用O表示该算法的时间复杂度?
分析:直接套用公式可知,a = 2, b = 2 ,f(n) = N/2 , 主定理主要和
n
log
b
a
n^{\log_b a}
nlogba做比较,带入可得
n
log
b
a
=
n
n^{\log_b a}= n
nlogba=n 。
这里需要注意,f(n)和
n
log
b
a
n^{\log_b a}
nlogba做比较 ,比较的是它们的渐近增长率,所以f(n)=
n
log
b
a
n^{\log_b a}
nlogba ,符合条件二,都是一次函数,所以T(n) = O(nlogn)。
例子3:
求下面代码的时间复杂度:
void Hanoi(int n, char a, char b, char c)//a为原始柱,b为借助柱,c为目标柱
{
if (n == 1)
{
Move(a, c);//只有一个盘子时直接移
}
else
{
Hanoi(n - 1, a, c, b);//将A柱子上n-1个盘子借助C柱子移到B上
Move(a, c);//将A最后一个盘子移到C上
Hanoi(n - 1, b, a, c);//将B柱子借助空A柱子移到C上
}
}
分析:我们可以看出,用递归来解决汉诺塔问题是非常方便的选择,最后我们来分析一下汉诺塔问题的时间复杂度。
设盘子个数为n时,需要T(n)步,把A柱子n-1个盘子移到B柱子,需要T(n-1)步,A柱子最后一个盘子移到C柱子一步,B柱子上n-1个盘子移到C柱子上T(n-1)步。 得递推公式T(n)=2T(n-1)+1 。这个递推式子不符合主定理,所以需要运用高中的基础数学知识,
由递推式可以知道,凑方法,凑成等比数列,凑成通项公式
O
(
2
n
)
O(2^n)
O(2n)
例子4:
假设问题规模为N,某一个递归算法的时间程度记T(N),已知T(1) = 0,T(N) = T(N- 1) + N,求用O表示该算法的时间复杂度?
分析:首先要看主定理的限定的条件,b > 1 才可以执行这个主定理,这里需要 T ( N ) = T ( N − 1 ) + N 变 成 T ( N ) − T ( N − 1 ) = N 。 可 以 T ( 1 ) , T ( 2 ) . . . . T ( N ) 叠 加 后 可 以 算 出 T ( N ) 的 通 项 公 式 。 可 以 计 算 O ( n 2 ) T(N) = T(N- 1) + N 变成 T(N) - T(N- 1) = N。 可以T(1) ,T(2) .... T(N) 叠加后可以算出T(N)的通项公式。 可以计算O(n^2) T(N)=T(N−1)+N变成T(N)−T(N−1)=N。可以T(1),T(2)....T(N)叠加后可以算出T(N)的通项公式。可以计算O(n2)
三、空间复杂度
类比于时间复杂度的讨论,一个算法的空间复杂度是指该算法所耗费的存储空间,计算公式计作:S(n) = O(f(n))。其中 n 也为数据的规模,f(n) 在这里指的是 n 所占存储空间的函数。一般情况下,我们的程序在机器上运行时,刨去需要存储程序本身的输入数据等之外,还需要存储对数据操作的「存储单元」。如果输入数据所占空间和算法无关,只取决于问题本身,那么只需要分析算法在实现过程中所占的「辅助单元」即可。如果所需的辅助单元是个常数,那么空间复杂度就是 O(1)。
参考:
https://www.jianshu.com/p/f4cca5ce055a
https://blog.csdn.net/qq_33274645/article/details/52688025
https://mp.weixin.qq.com/s/9njtnqfAatjmjPh4geETqA