所有Leetcode题目不定期汇总在 Github, 欢迎大家批评指正,讨论交流。
趁假期复习了算法基础的时间复杂度和空间复杂度,整理一遍。
原文发布于个人博客(好望角),并在博客持续修改更新,此处可能更新不及时。
要想理解时间复杂度和空间复杂度这两个概念,首先要明白算法的含义。
所谓算法,是解决一类问题的通法,即一系列清晰无歧义的计算指令。
具体的,一个算法应该有以下五个方面的特性:
根据以上的定义,不难发现。每个算法只能解决具有特定特征的一类问题。然而,每个有固定输入输出的问题可以采取多种算法来决解。
那么,要怎么来比较解决同一个问题的不同算法之间的优劣呢?
这个时候,时间复杂度和空间复杂度就有了用武之地。
算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的优劣与否。
验证算法的时间复杂度,我们有以下两个方法。
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。所以就有了事后统计的方法。
计算算法的时间复杂度,往往是为了评测算法的性能,设计更好的算法。这就给事后统计的方法带来了两个弊端。
由于事后统计的方法有上述的弊端,我们通常采取事先估计的方法来评价算法的时间复杂度。
为了更好的比较不同算法在处理统一问题上的效率,通常从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作的重复执行的次数作为算法的时间量度,记为T(n)。
在这里,n为输入问题的规模。对于同一个问题来说,他的输入规模越大,往往时间复杂度也就越大。
关于输入问题规模n,有辅助函数f(n),来统计算法基本操作的频度。因此,算法的时间复杂度往往记为 T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))。
为了简便,我们一般在计算时间复杂度往往选取最简单的f(n)表示。例如: O ( 2 n 2 + n + 1 ) = O ( 3 n 2 + n + 3 ) = O ( 7 n 2 + n ) = O ( n 2 ) O(2n^2+n+1) = O (3n^2+n+3) = O(7n^2+n) = O(n_2) O(2n2+n+1)=O(3n2+n+3)=O(7n2+n)=O(n2) ,一般都只用 O ( n 2 ) O(n_2) O(n2)表示就可以了。
也就是说,两个算法的时间频度不一样,但很有可能拥有相同的时间复杂度。
例如: T ( n ) = n 2 + 3 n + 4 T(n)=n^2+3n+4 T(n)=n2+3n+4 与 T ( n ) = 4 n 2 + 2 n + 1 T(n)=4n^2+2n+1 T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为 O ( n 2 ) O(n^2) O(n2)。
常见的算法时间复杂度由小到大依次为:
O ( 1 ) < O ( l o g 2 ( n ) ) < O ( n ) < O ( n l o g 2 ( n ) ) < O ( n 2 ) < O ( n 3 ) < . . . < O ( n ! ) O(1)<O(log_2(n))<O(n)<O(nlog_2(n))<O(n^2)<O(n^3)<...<O(n!) O(1)<O(log2(n))<O(n)<O(nlog2(n))<O(n2)<O(n3)<...<O(n!)
下面的图片直观的表示他们之间复杂度关系。
乘法法则: 是指若算法的2个部分时间复杂度分别为 T 1 ( n ) = O ( f ( n ) ) T_1(n)=O(f(n)) T1(n)=O(f(n))和 T 2 ( n ) = O ( g ( n ) ) T_2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 T 2 = O ( f ( n ) g ( n ) ) T_1 T_2=O(f(n) g(n)) T1T2=O(f(n)g(n))
求和法则:是指若算法的2个部分时间复杂度分别为 T 1 ( n ) = O ( f ( n ) ) T_1(n)=O(f(n)) T1(n)=O(f(n)) 和 T 2 ( n ) = O ( g ( n ) ) T_2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ( n ) + T 2 ( n ) = O ( m a x ( f ( n ) , g ( n ) ) ) T_1(n)+T_2(n)=O(max(f(n), g(n))) T1(n)+T2(n)=O(max(f(n),g(n)))
特别地,若 T 1 ( m ) = O ( f ( m ) ) T_1(m)=O(f(m)) T1(m)=O(f(m)), T 2 ( n ) = O ( g ( n ) ) T_2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ( m ) + T 2 ( n ) = O ( f ( m ) + g ( n ) ) T_1(m)+T_2(n)=O(f(m)+g(n)) T1(m)+T2(n)=O(f(m)+g(n))
Temp=i;
i=j;
j=temp;
如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是$O(1)$。
sum=0; (一次)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
sum++; (n^2次)
一般情况下,循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判别、控制转移等成分,当有若干个循环语句嵌套时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
for (i=1;i
a=0; ①
b=1; ①
for (i=1;i<=n;i++) ②
{
s=a+b; ③
b=a; ④
a=s; ⑤
}
i=1; ①
while (i<=n)
i=i*2;
for(i=0;i
设计算法的时候,我们还会关注空间复杂度,空间复杂度是算法在运行过程中临时占用的存储空间大小的度量, 同样是关于问题规模n的函数。
但根本上,算法的时间运行效率才是最重要的。只要算法占用的存储空间不要达到计算机无法接受的程度即可。所以,常常通过牺牲空间复杂度来换取算法更加高效的运行时间效率。
算法在计算机存储器上占用的空间包括三个部分。
算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不会随算法的不同而改变。这不是我们需要考虑的部分。
存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这部分存储空间,就必须编写出较短的算法。然而,算法想要实际应用需要根据需求采取不同的编程语言来实现,不同编程语言实现的代码长短差别很大,然而存储空间都在可接受范围之内(通常不同编程语言的效率更受关注)。
根据算法在运行过程中临时占用存储空间的不同,可以将算法分为两类。
假设我们想要将拥有n个项目的数组反过来。一个最简单作这件事的方式是这样:
function reverse(a[0..n])
allocate b[0..n]
for i from 0 to n
b[n - i] = a[i]
return b
不幸地,这样需要 O ( n ) O(n) O(n)的空间来创建b数组,且配置存储器通常是一件缓慢的运算。如果我们不再需要a,我们可使用这个原地算法,用它自己反转的内容来覆盖掉:
function reverse-in-place(a[0..n])
for i from 0 to floor(n/2)
swap(a[i], a[n-i])
了解算法的时间复杂度和空间复杂度之后,再看一些常用算法总结的时候就不会再向原来一样有雾里探花之感了。
算法的时间复杂度和空间复杂度-总结
原地算法
所有Leetcode题目不定期汇总在 Github, 欢迎大家批评指正,讨论交流。