概述
介绍算法时间复杂度和空间复杂度
1. 简介
1.1 算法分析简介
算法分析:第一 是从数学上证明算法的正确性
,这一步主要用到形式化证明的方法及相关推理模式,如循环不变式、数学归纳法等。而在证明算法是正确的基础上,第二 就是分析算法的时间复杂度
。算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的优劣与否。
1.2 算法执行时间度量
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。
而度量一个程序的执行时间通常有两种方法:
-
事后统计的方法:这种方法可行,但不是一个好的方法。该方法有两个缺陷:
一是要想对设计的算法的运行性能进行评测,必须先依据算法编制相应的程序并实际运行;
二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优势。 -
事前分析估算的方法:在编写程序前,依据统计方法对算法进行估算。一个用高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 算法采用的策略、方法;
- 编译产生的代码质量;
- 问题的输入规模;
- 机器执行指令的速度。
一个算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果。为了便于比较同一个问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作的重复执行的次数作为算法的时间量度。
2. 时间复杂度
2.1 时间复杂度简介
时间频度
: 一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。时间复杂度
: 在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数
。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
2.2 时间复杂度排序
在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1),另外,在时间频度不相同时,时间复杂度有可能相同【如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为 O ( n 2 ) O(n^2) O(n2)】。 按数量级递增排列,常见的时间复杂度有:
常数阶 O ( 1 ) O(1) O(1),对数阶 O ( l o g 2 n ) O(log_2n) O(log2n),线性阶 O ( n ) O(n) O(n), 线性对数阶 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),平方阶 O ( n 2 ) O(n^2) O(n2),立方阶 O ( n 3 ) O(n^3) O(n3)… k次方阶 O ( n k ) O(n^k) O(nk),指数阶 O ( 2 n ) O(2^n) O(2n)…
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
从图中可见,我们应该尽可能选用多项式阶O(nk)的算法,而不希望用指数阶的算法。
常见的算法时间复杂度由小到大依次为:
O
(
1
)
<
O
(
l
o
g
2
n
)
<
O
(
n
)
<
O
(
n
l
o
g
2
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
…
<
O
(
2
n
)
<
O
(
n
!
)
Ο(1)<Ο(log_2n)<Ο(n)<Ο(nlog_2n)<Ο(n^2)<Ο(n^3)<…<Ο(2^n)<Ο(n!)
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<…<O(2n)<O(n!)
一般情况下,对一个问题(或一类算法)只需选择一种基本操作来讨论算法的时间复杂度即可,有时也需要同时考虑几种基本操作,甚至可以对不同的操作赋予不同的权值,以反映执行不同操作所需的相对时间,这种做法便于综合比较解决同一问题的两种完全不同的算法。
2.3 时间复杂度求解
求解算法的时间复杂度的具体步骤是:
-
找出算法中的基本语句;
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。 -
计算基本语句的执行次数的数量级;
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。 -
用大Ο记号表示算法的时间性能。
将基本语句执行次数的数量级放入大 O Ο O记号中。 -
在计算算法时间复杂度时有以下几个简单的程序分析法则:
4.1 对于一些简单的输入输出语句或赋值语句,近似认为需要 O ( 1 ) O(1) O(1)时间
4.2 对于顺序结构,需要依次执行一系列语句所用的时间可采用大 O O O下"求和法则"
4.3 对于选择结构,如if语句,它的主要时间耗费是在执行then字句或else字句所用的时间,需注意的是检验条件也需要 O ( 1 ) O(1) O(1)时间
4.4 对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大 O O O下"乘法法则"
4.5 对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度
求和法则
:是指若算法的2个部分时间复杂度分别为 T 1 ( n ) = O ( f ( n ) ) T1(n)=O(f(n)) T1(n)=O(f(n))和 T 2 ( n ) = O ( g ( n ) ) T2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ( n ) + T 2 ( n ) = O ( m a x ( f ( n ) , g ( n ) ) ) T1(n)+T2(n)=O(max(f(n), g(n))) T1(n)+T2(n)=O(max(f(n),g(n)))
特别地,若 T 1 ( m ) = O ( f ( m ) ) T1(m)=O(f(m)) T1(m)=O(f(m)),, T 2 ( n ) = O ( g ( n ) ) T2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ( m ) + T 2 ( n ) = O ( f ( m ) + g ( n ) ) T1(m)+T2(n)=O(f(m) + g(n)) T1(m)+T2(n)=O(f(m)+g(n))
乘法法则
: 是指若算法的2个部分时间复杂度分别为 T 1 ( n ) = O ( f ( n ) ) T1(n)=O(f(n)) T1(n)=O(f(n))和 T 2 ( n ) = O ( g ( n ) ) T2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ∗ T 2 = O ( f ( n ) ∗ g ( n ) ) T1*T2=O(f(n)*g(n)) T1∗T2=O(f(n)∗g(n))
2.4 常见时间复杂度示例说明
2.4.1 空间复杂度 O ( 1 ) O(1) O(1)
Temp=i; i=j; j=temp;
以上三条单个语句的频度均为1,该程序段的执行时间是一个与问题规模n无关的常数。算法的时间复杂度为常数阶,记作
T
(
n
)
=
O
(
1
)
T(n)=O(1)
T(n)=O(1)。注意:如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是
O
(
1
)
O(1)
O(1)。
2.4.2 空间复杂度 O ( n 2 ) O(n^2) O(n2)
示例一、交换i和j的内容
sum=0; (一次)
for(i=1;i<=n;i++) (n+1次)
for(j=1;j<=n;j++) (n^2次)
sum++; (n^2次)
解:因为Θ(2n2+n+1)=n2(Θ即:去低阶项,去掉常数项,去掉高阶项的常参得到),所以T(n)= =O(n2);
for (i=1;i<n;i++)
{
y=y+1; ①
for (j=0;j<=(2*n);j++)
x++; ②
}
解: 语句1的频度是
n
−
1
n-1
n−1
语句2的频度是
(
n
−
1
)
∗
(
2
n
+
1
)
=
2
n
2
−
n
−
1
(n-1)*(2n+1)=2n^2-n-1
(n−1)∗(2n+1)=2n2−n−1
f
(
n
)
=
2
n
2
−
n
−
1
+
(
n
−
1
)
=
2
n
2
−
2
f(n)=2n^2-n-1+(n-1)=2n^2-2
f(n)=2n2−n−1+(n−1)=2n2−2;
又Θ(2n2-2)=n2
该程序的时间复杂度
T
(
n
)
=
O
(
n
2
)
T(n)=O(n^2)
T(n)=O(n2).
一般情况下,对步进循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判别、控制转移等成分,当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
2.4.3 空间复杂度 O ( n ) O(n) O(n)
a=0;
b=1; ①
for (i=1;i<=n;i++) ②
{
s=a+b; ③
b=a; ④
a=s; ⑤
}
解: 语句1的频度:2,
语句2的频度: n,
语句3的频度: n-1,
语句4的频度:n-1,
语句5的频度:n-1,
T
(
n
)
=
2
+
n
+
3
(
n
−
1
)
=
4
n
−
1
=
O
(
n
)
T(n)=2+n+3(n-1)=4n-1=O(n)
T(n)=2+n+3(n−1)=4n−1=O(n).
2.4.4 空间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)
i=1; ①
while (i<=n)
i=i*2; ②
解: 语句1的频度是1,
设语句2的频度是f(n), 则:
2
f
(
n
)
<
=
n
2^{f(n)}<=n
2f(n)<=n;
f
(
n
)
<
=
l
o
g
2
n
f(n)<=log_2n
f(n)<=log2n
取最大值
f
(
n
)
=
l
o
g
2
n
f(n)=log_2n
f(n)=log2n,
T
(
n
)
=
O
(
l
o
g
2
n
)
T(n)=O(log_2n)
T(n)=O(log2n)
2.4.5 空间复杂度 O ( n 3 ) O(n^3) O(n3)
for(i=0;i<n;i++)
{
for(j=0;j<i;j++)
{
for(k=0;k<j;k++)
x=x+2;
}
}
解:当i=m, j=k的时候,内层循环的次数为k当i=m时, j 可以取 0,1,…,m-1 , 所以这里最内循环共进行了0+1+…+m-1=(m-1)m/2次所以,i从0取到n, 则循环共进行了: 0+(1-1)*1/2+…+(n-1)n/2=n(n+1)(n-1)/6所以时间复杂度为
O
(
n
3
)
O(n^3)
O(n3).
2.5 常用的算法的时间复杂度和空间复杂度
一个经验规则:其中c是一个常量,如果一个算法的复杂度为c 、
l
o
g
2
n
log_2n
log2n 、n 、
n
∗
l
o
g
2
n
n*log_2n
n∗log2n ,那么这个算法时间效率比较高 ,如果是
2
n
2^n
2n,
3
n
3^n
3n ,
n
!
n!
n!,那么稍微大一些的n就会令这个算法不能动了,居于中间的几个则差强人意。
算法时间复杂度分析是一个很重要的问题,任何一个程序员都应该熟练掌握其概念和基本方法,而且要善于从数学层面上探寻其本质,才能准确理解其内涵。
3. 空间复杂度
类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。渐近空间复杂度也常常简称为空间复杂度。
3.1 基本概念
定义
:空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。
3.2 存储空间度量
存储空间
:一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
存储算法本身所占用的存储空间
与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法算法的输入输出数据所占用的存储空间
是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。算法在运行过程中临时占用的存储空间
随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地"进行的,是节省存储的算法,如这一节介绍过的几个算法都是如此;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如将在第九章介绍的快速排序和归并排序算法就属于这种情况。
当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为
O
(
1
)
O(1)
O(1);
当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n);
当一个算法的空I司复杂度与n成线性比例关系时,可表示为
O
(
n
)
O(n)
O(n)。
若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
3.3 常见空间复杂度示例
空间复杂度比较常用的有:O(1)、O(n)、O(n2),我们下面来看看:
3.3.1 空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
举例:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
3.3.2 空间复杂度 O(n)
我们先看一个代码:
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:
for (i=1; i<=n; i++)
x++;
for (i=1; i<=n; i++)
for (j=1; j<=n; j++)
x++;
第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(n2),则整个算法的时间复杂度为Ο(n+n2)=Ο(n2) 。