算法和算法分析
之前盲目的学了一些链表、二叉树、图论之类的数据结构,都是零碎的,不成体系,囫囵吞枣,没有深入理解,也不会用,在这里补充一些概念性的东西。
基本概念
算法的定义:是对特定问题求解步骤的一种描述。算法是指令的有限序列,其中每条指令表示一个或多个操作。
算法的特性
- 有穷性:算法要在有穷时间和有穷步骤内完成。
- 确定性:算法里每一个语句都要有确定的含义。
- 可行性:每一条语句都是可行的。
- 输入:可以是0个,1个或多个。
- 输出:至少有一个。
算法和程序
程序不一定满足有穷性。程序中的指令必须是机器可执行的,算法的指令无此限制。算法代表了对问题的解,而程序则是算法在计算机上的特定的实现。
一个算法用若用程序设计语言来描述,就是一个程序。
算法与数据结构
二者相辅相成。解决某一特定的算法可以选定不同的数据结构,而且选择恰当与否直接影响算法的效率。反之,一个数据结构的优劣由各种算法的执行来体现。
算法设计的要求
评价算法的标准:
- 正确性
- 可读性
- 健壮性
- 效率与存储量要求:
- 效率指算法运行时间
- 存储量指算法运行时占用的最大存储空间
正确性:实际情况下,无法把所有情况都输入到算法,也不能随便选择几个输入进行测试。所以要选择典型的、苛刻的、边缘的输入进行测试,看是否能得出正确的结果。
可读性和健壮性没什么说的,也很重要。
前三点没有具体的衡量标准,但是 效率与存储量要求是有方法衡量的。
算法效率的衡量方法和准则:
通常有两种衡量方法:
- 事后统计法,一般不采用,主要有两个缺点:
- 必须执行程序
- 其它因素掩盖算法本质
- 事前分析估算法
和算法执行时间相关的因素:
- 算法选用的策略(算法的思想)
- 问题的规模
- 编写程序的语言
- 编译程序产生的机器代码质量
- 计算机执行指令的速度
一个特定算法的“运算工作量”的大小,只依赖于问题的规模(通常用整数量
n
n
n表示),或者说,它是问题规模的函数。
假如,随着问题规模
n
n
n的增长,算法执行时间增长率和
f
(
n
)
f(n)
f(n)的增长率相同,则可记作:
T
(
n
)
=
O
(
f
(
n
)
)
T(n)=O(f(n))
T(n)=O(f(n))
称 T ( n ) T(n) T(n)是算法的 (渐进)时间复杂度。
如何估算算法的时间复杂度
算
法
=
控
制
结
构
+
原
操
作
(
固
有
类
型
操
作
)
算法 = 控制结构 + 原操作(固有类型操作)
算法=控制结构+原操作(固有类型操作)
算
法
的
执
行
时
间
=
原
操
作
(
i
)
的
执
行
次
数
×
原
操
作
(
i
)
的
执
行
时
间
算法的执行时间=原操作(i)的执行次数\times原操作(i)的执行时间
算法的执行时间=原操作(i)的执行次数×原操作(i)的执行时间
算法的执行时间与原操作执行次数之和成正比
从算法中选取一种对于所研究的问题来说是基本操作的原操作,以该基本操作在算法中重复执行的次数作为算法运行时间的衡量准则。
频度:指该语句重复执行的次数。
常数阶:
例1.
{++x;s=0;}
将x自增看成基本操作,则语句频度为1,即时间复杂度为
O
(
1
)
O(1)
O(1)。
如果将s=0
也看成基本操作,则语句频度为2,其时间复杂度仍为
O
(
1
)
O(1)
O(1),即常数阶。
线性阶:
例2.
for(int i=0;i<n;++i)
{
++x;
s+=x;
}
语句频度为:
2
n
2n
2n。
其时间复杂度为:
O
(
n
)
O(n)
O(n),即线性阶。
对数阶:
例3.
void f(int n)
{
int x=1;
while(x<n)
x=2*x;
}
分析:基本运算语句是x=2*x
,设其运行时间为
T
(
n
)
T(n)
T(n),则满足
2
T
(
n
)
≤
n
2^{T(n)}\le n
2T(n)≤n,即
T
(
n
)
≤
l
o
g
2
n
=
O
(
l
o
g
2
n
)
T(n)\le log_2n=O(log_2n)
T(n)≤log2n=O(log2n),所以其时间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n),即对数阶。
平方阶:
例4.
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
++x;
s+=x;
}
}
语句频度为:
2
n
2
2n^2
2n2
时间复杂度为:
T
(
n
)
=
O
(
n
2
)
T(n)=O(n^2)
T(n)=O(n2),即平方阶。
立方阶:
例5.
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
c[i][j]=0;
for(int k=0;k<n;k++)
{
c[i][j]+=a[i][k]*b[k][j];
}
}
}
难点:多层嵌套循环中基本语句的选择,基本语句只选择最内层循环的语句。
所以,语句频度为:
n
×
n
×
n
=
n
3
n\times n\times n=n^3
n×n×n=n3
时间复杂度为:
T
(
n
)
=
O
(
n
3
)
T(n)=O(n^3)
T(n)=O(n3)
定理:若多项式
A
(
n
)
=
a
m
n
m
+
a
m
−
1
n
m
−
1
+
.
.
.
+
a
1
n
+
a
0
A(n)=a_mn^m+a_{m-1}n^{m-1}+...+a_1n+a_0
A(n)=amnm+am−1nm−1+...+a1n+a0,则
A
(
n
)
=
O
(
n
m
)
A(n)=O(n^m)
A(n)=O(nm)
例6.
for(int i=0;i<n;i++)
{
for(int j=0;j<i;j++)
{
++x;
a[i][j]=x;
}
}
i=0时,内层循环执行0次;
i=1时,内层循环执行1次;
i=2时,内层循环执行2次;
… …
i=n-1时,内层循环执行n-1次。
语句频度为:
0
+
1
+
2
+
.
.
.
+
(
n
−
1
)
=
0
+
(
n
−
1
)
2
×
n
=
1
2
(
n
2
−
n
)
0+1+2+...+(n-1)=\frac {0+(n-1)}{2}\times n=\frac 1 2(n^2-n)
0+1+2+...+(n−1)=20+(n−1)×n=21(n2−n)
所以时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),即平方阶。
总结:推导大O阶方法
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,去除最高项前的常数。
平均时间复杂度和最坏时间复杂度
对于算法的分析:
- 一种方法是计算所有情况的平均值,这种方法称为平均时间复杂度。
平均时间复杂度是所有情况中最有意义的,因为它是期望的运行时间。但是现实中,平均运行时间很难通过分析得到,一般是通过运行一定数量的实验数据后估算出来的。 - 另一种方法是计算最坏情况下的时间复杂度,称为最坏时间复杂度。
最坏运行时间是一种保证,那就是运行时间不会再坏了。
一般在没有特殊说明的情况下,都是指最坏时间复杂度。
时间复杂度比较关系式
多项式时间关系:
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
l
o
g
n
)
<
O
(
n
2
)
<
O
(
n
3
)
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)
指数时间关系:
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(2^n)<O(n!)<O(n^n)
O(2n)<O(n!)<O(nn)
利用matplotlib看一下曲线
#coding:utf-8
import matplotlib.pyplot as plt
import numpy as np
from numpy.lib.scimath import logn
from math import e
import matplotlib as mpl
#防止中文乱码问题
mpl.rcParams['font.sans-serif']=[u'SimHei']
mpl.rcParams['axes.unicode_minus']=False
n=np.linspace(1,100,100) #创建100个1到100之间的等差数列
plt.plot(x,np.ones(len(x)),'chocolate',linewidth=2, label=u'1')
plt.plot(x,logn(2,x),'g-',linewidth=2, label=u'logn')
plt.plot(x,x,'b-',linewidth=2, label=u'n')
plt.plot(x,x*logn(2,x),'r-',linewidth=2, label=u'nlogn')
plt.plot(x,x*x,'darkred',linewidth=2, label=u'n^2')
plt.plot(x,x*x*x,'y-',linewidth=2, label=u'n^3')
plt.axis([-3, 100, -3., 100])#指定画图板的长宽
plt.legend(loc = 'lower right') #图例的位置
plt.grid(True)#需要网格
plt.show()
当n很大时,指数时间算法和多项式时间算法在时间消耗上相差十分悬殊。
例如,
import math
n=100
print ("%e"%math.pow(n,3)) #多项式,为了和指数对比
print ("%e"%math.pow(2,n))
print ("%e"%math.factorial(n))
print ("%e"%math.pow(n,n))
因此,只要有人能将现有指数时间算法中的任何一个算法转化成多项式时间算法,那就取得了一个伟大的成就。
有的情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同。
例7.
int array[]={6,1,3,4,2};
void bubbleSort(int array[], int n)
{
for (int i = 0,change=true; i < n&&change; i++)
{
change = false;
for (int j = 0; j < n - i - 1; j++)
{
if(array[j]>array[j + 1])
{
int tmp = array[j + 1];
array[j + 1] = array[j];
array[j] = tmp;
change = true;
}
}
}
}
上面是一个冒泡排序,通过change
可以使排序提前终止。选择if语句内语句作为基本语句。最好的结果是不发生任何交换,仅需要比较n-1次,时间复杂度是
O
(
n
)
O(n)
O(n);最坏的结果是输入是逆序数列,需要比较
1
+
2
+
3
+
+
.
.
.
+
n
−
1
=
1
/
2
∗
(
n
2
−
n
)
1+2+3++...+n-1=1/2*(n^2-n)
1+2+3++...+n−1=1/2∗(n2−n)次,每次都进行交换,时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)。通过大量实验,平均时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)。
归纳总结
例8:已知程序有4个并列的程序段,它们的时间复杂度分别为
T
1
(
n
)
=
O
(
1
)
,
T
2
(
n
)
=
O
(
n
)
,
T
3
(
n
)
=
O
(
n
2
)
,
T
4
(
n
)
=
O
(
2
n
)
T_1(n)=O(1),T_2(n)=O(n),T_3(n)=O(n^2),T_4(n)=O(2^n)
T1(n)=O(1),T2(n)=O(n),T3(n)=O(n2),T4(n)=O(2n),整个程序的时间复杂度是多少?
分析:
T
(
n
)
=
T
1
(
n
)
+
T
2
(
n
)
+
T
3
(
n
)
+
T
4
(
n
)
=
O
(
m
a
x
{
1
,
n
,
n
2
,
2
n
}
)
=
O
(
2
n
)
T(n)=T_1(n)+T_2(n)+T_3(n)+T_4(n)=O(max\{1,n,n^2,2^n\})=O(2^n)
T(n)=T1(n)+T2(n)+T3(n)+T4(n)=O(max{1,n,n2,2n})=O(2n)
例9:已知一个程序的时间复杂度为
T
1
(
n
)
=
O
(
n
)
T_1(n)=O(n)
T1(n)=O(n)。其中调用了两个子函数,一个子函数的时间复杂度是
T
2
(
n
)
=
O
(
l
o
g
n
)
T_2(n)=O(logn)
T2(n)=O(logn),另一个子函数的时间复杂度是
T
3
(
n
)
=
O
(
n
2
)
T_3(n)=O(n^2)
T3(n)=O(n2),整个程序的时间复杂度应该是多少?
分析:
T
(
n
)
=
T
1
(
n
)
×
(
T
2
(
n
)
+
T
3
(
n
)
)
=
O
(
n
×
m
a
x
{
l
o
g
n
,
n
2
}
)
=
O
(
n
3
)
T(n)=T_1(n)\times (T_2(n)+T_3(n))=O(n\times max\{logn,n^2\})=O(n^3)
T(n)=T1(n)×(T2(n)+T3(n))=O(n×max{logn,n2})=O(n3)
算法的存储空间需求
算法的空间复杂度定义为: S ( n ) = O ( g ( n ) ) S(n)=O(g(n)) S(n)=O(g(n))
表示随着问题规模 n n n的增大,算法运行所需存储量的增长率与g(n)的增长率相同。
算法的存储量包括:
- 输入数据所占的空间
- 程序本身所占的空间
- 辅助变量所占的空间
1和2基本固定,我们想要求的其实就是辅助变量所占的空间。
若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。
若所需额外空间相对于输入数据量来说是常数,则称算法是"原地"工作的。
若所需存储量依赖于特定的输入,则通常按最坏情况考虑。
递归栈空间占用
递归算法中,每一次递推需要一个栈空间来保存调用记录。因此,空间复杂度需要计算递归栈的辅助空间。
递推树的深度就是它使用的栈空间的大小。