时间复杂度和空间复杂度

476 阅读6分钟

时间复杂度

概念

程序是由控制结构(顺序,分支,循环)和原操作(固有数据类型的操作)组成,算法的时间取决于两者的综合效果,为了探究同一问题的不同算法,选取该程序中处理数据的基本操作,作为原操作,该操作执行的次数作为算法的时间量度。

1. 方法

  • 事后统计

利用计算机的计时功能,有的甚至能精确到毫秒,不同算法可以通过一组或若干组相同的统计数据来区分优劣,但是这种方法有两种缺陷:

  1. 必须先运行算法编制的程序
  2. 所得时间的统计了依赖于计算机的硬件软件等环境因素,有事同意掩盖算法本身的优劣
  • 事前分析

一个高级程序语言编写的程序运行所消耗的时间取决于以下因素:

  1. 算法策略
  2. 问题规模
  3. 书写的语言,语言越高级,效率越低
  4. 编译器产生的机器代码的质量
  5. 硬件

2. 符号

算法复杂度分析中的符号:

  • \Theta 读音theta,等于的意思
  • O 读音big-oh,上界,小于等于
  • o 读音small-oh,上界,小于
  • \Omega 读音big-omega,下界,大于等于
  • \omega 读音small-omega,下界,大于

O就是最坏的情况,一般比较算法的优劣,优先比较O。我们用T(n)来表示时间复杂度。

2. 计算

寻找基本语句,通常是程序里面最复杂,执行此处最多的,实现主要功能的语句。然后根据代码逻辑,计算其执行次数。例如,单条语句的执行次数是1

a += 1
a += 1
a += 1  # 这个python程序的执行次数是3

通常探究的,是循环这类重复操作的执行效率。

sum = 0
for i in range(n):
	sum = sum + i

上面这个单层循环求和算法的基本操作为加法,这里引入一个函数f(n),用其表示程序基本操作的执行次数,这个执行次数用数学表达式可以表示为:

f(n) = 1 * n

n趋向于无穷大时,{T(n)}\over{f(n)}的极限值为不等于0的常数,即\lim_{n \to \infty}{ \frac {T(n)}{f(n)}}=C,则称f(n)T(n)的同数量级函数,记为T(n)=O(f(n)),称为算法的渐进时间复杂度,简称时间复杂度。

则,此算法的时间复杂度表示为:

T(n)=O(n)

如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数C。而时间复杂度是近似值,是一个极限,他研究的是当问题规模n无限增大的情况,即n趋向于无穷大的情况,相比于无穷大的n,常数C可以忽略,取其系数1,所以此类算法的时间复杂度表示为T(n) = O(1),即上面第一段代码的时间复杂度。

深究

下面是一个c*n阶矩阵的计算,c是常数,用双层循环实现:

for i in range(n):
	for j in range(n):
		arr[i] += c * a[j][i]

当有若干个循环语句时,算法的时间复杂度一般是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。

这个循环的基本操作是arr[i] += c * a[j][i],执行的次数为:

f(n) = n * n

时间复杂度T(n) = O(f(n))。这个双层循环的时间复杂度就是T(n) = O(n^2)

一般来说如果内外循环没有关系,可将内外循环次数之积作为时间复杂度来看,如果我们的循环内外有关联,时间复杂度的计算变得稍微复杂,需要考虑基本操作执行的次数,例如下面这个双层循环:

sum = 0
for i in range(n):
	for j in range(n-i):
        sum += j

这是一个内外循环相关的双层循环,他的计算次数可以进行推导:

f(n) = n+(n-1)+(n-2)+...+3+2+1
={{(n+1)*n}\over{2}}={{n^2+n}\over{2}}={{1}\over{2}}n^2+{{1}\over{2}}n

我们取对算法影响最大的一项,当n趋向于无穷大时,f(n)的极限就是n^2,即多项式中次数最大的项除去系数n^2,它的时间复杂度为T(n) = O(n^2)

另一个例子:

i = 1
while i < n:
	i = i * 2

上面代码的基本操作是i = i * 2,当i>n时结束循环,假设x为执行的次数,i的值为2^x,反推x即为算法执行次数,f(n)=x=\log(n),他的时间复杂度度可以表示为:T(n)=O(\log(n))

三层循环

通常来计算时间复杂度,都会用到数学归纳法,找运算执行次数的规律,下面看一个稍微复杂一点的例子:

for i in range(n):
	for j in range(i):
		for k in range(j):
			++m

这是一个三重循环,以最内部循环里面的数据操作为基本操作++m,推导他的计算次数:

S = 1+(1+2)+(1+2+3)+...+(1+2+...+n) S=n+(n-1)*2+(n-2)*3+...+2*(n-1)+n S=1+2+...+(n-1)+n   +1+2+...+(n-2)+(n-1)   +1+2+...+(n-3)+(n-2)   +1+2+...+(n-3)+......   +1+2+3   +1+2   +1

每一行相加得到

S=1+{{2^2+2}\over{2}}+{{3^2+3}\over{2}}+{{4^2+4}\over{2}}+...+{{(n-1)^2+(n-1)}\over{2}}+{{n^2+n}\over{2}}

裂项相加得

S={{{[n+(n-1)+...+3+2+1]+[n^2+(n-1)^2+...+3^2+2^2+1^2]}}\over{{2}}}

1^2+2^2+...+n^2={{1}\over{6}}n(2n+1)(n+1),代入多项式得

S={{1}\over{6}}n^3+{{1}\over{2}}n^2+{{1}\over{3}}n

{{1}\over{6}}n(2n+1)(n+1)推导过程在文末给出

取其中对运行次数影响最大的一项{{1}\over{6}}n^3,当n趋向于无穷大时,{{1}\over{6}}n^3=n^3,所以这个程序基本代码的运行次数是f(n)=n^3,时间复杂度可以表示为T(n)=O(n^3)

列举

下面列举以下几个排序算法的复杂度: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mcJwEESn-1573030192407)(github.com/belingud/im…)]

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2)


空间复杂度

概念

一个程序除了需要存储空间来寄存本身所用指令、常熟、变量和输入数据局外,也需要一些对数据进行操作的工作单元和存储一些为实现计算所需的辅助空间,称为算法的空间复杂度,表示的是存储空间跟数据输入规模的增长关系。如果额外空间相对于输入数据量来说是常数,则称此算法原地工作。比如排序。

表达式为:

S(n)=f(n)

案例:

baz = 1

这个程序运行占用空间为1个,额外空间占用为0,程序原地执行,则空间复杂度是S(n)=O(1)

分析

代码占用空间的分析,以一个变量交换值的代码为例:

foo = 1
bar = 2
tmp = None
tmp = foo
foo = bar
bar = tmp

这段代码占用了一个额外空间来存储变量tmp,可以使用foo, bar = bar, foo可以使代码更简洁,同时减少内存空间。





推导过程

我们有(n+1)^3=n^3+3n^2+3n+1,于是展开得: 2^3=1^3+3*1^2+3*1+1 3^3=2^3+3*2^2+3*2+1 ... (n+1)^3==n^3+3n^2+3n+1

将所有的行相加得,

2^3+3^3+4^3+...+(n-1)^3+n^3+(n+1)^3=(1^3+2^3+3^3+...+n^3)+3(1^2+2^2+3^2+...+n^2)+3(1+2+3+...+n)+n

消去左右的重复项2^3+3^3+...+n^3,得

(n+1)^3=1^3+3(1^2+2^2+3^2+...+n^2)+3(1+2+3+...+n)+n (n+1)^3-1^3-{{3(n+1)n}\over{2}}-n=3(1^2+2^2+3^3+...+n^2)

解得:

1^2+2^2+3^2+...+n^2={{1}\over{6}}(2n+1)(n+1)