算法时间复杂度
文章目录
算法复杂度
算法复杂度分为时间复杂度和空间复杂度。其作用:
- 时间复杂度是指执行这个算法所需要的计算工作量;
- 空间复杂度是指执行这个算法所需要的内存空间;
时间复杂度基础概念
在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
算法运算次数
我们假设计算机运行一行基础代码需要执行一次运算。
|
|
那么上面这个方法需要执行 2 次运算
|
|
这个方法需要 (n + 1 + n + 1) = 2n + 2 次运算。
我们把 算法需要执行的运算次数 用 输入大小n 的函数 表示,即 T(n) 。
由算法运算次数推出时间复杂度
-
我们知道常数项对函数的增长速度影响并不大,所以当 T(n) = c,c 为一个常数的时候,我们说这个算法的时间复杂度为 O(1);如果 T(n) 不等于一个常数项时,直接将常数项省略。
1 2 3
比如 第一个 Hello, World 的例子中 T(n) = 2,所以我们说那个函数(算法)的时间复杂度为 O(1)。 T(n) = n + 29,此时时间复杂度为 O(n)。
-
我们知道高次项对于函数的增长速度的影响是最大的。n^3 的增长速度是远超 n^2 的,同时 n^2 的增长速度是远超 n 的。 同时因为要求的精度不高,所以我们直接忽略低此项。
1 2
比如 T(n) = n^3 + n^2 + 29,此时时间复杂度为 O(n^3)。
-
因为函数的阶数对函数的增长速度的影响是最显著的,所以我们忽略与最高阶相乘的常数。
1 2
比如 T(n) = 3n^3,此时时间复杂度为 O(n^3)。
综合起来:如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n))。
四个便利的法则/案例
-
对于一个循环,假设循环体的时间复杂度为 O(n),循环次数为 m,则这个循环的时间复杂度为 O(n×m)。
1 2 3 4 5
void aFunc(int n) { for(int i = 0; i < n; i++) { // 循环次数为 n printf("Hello, World!\n"); // 循环体时间复杂度为 O(1) } }
此时时间复杂度为 O(n × 1),即 O(n)。
-
对于多个循环,假设循环体的时间复杂度为 O(n),各个循环的循环次数分别是a, b, c…,则这个循环的时间复杂度为 O(n×a×b×c…)。分析的时候应该由里向外分析这些循环。
1 2 3 4 5 6 7
void aFunc(int n) { for(int i = 0; i < n; i++) { // 循环次数为 n for(int j = 0; j < n; j++) { // 循环次数为 n printf("Hello, World!\n"); // 循环体时间复杂度为 O(1) } } }
此时时间复杂度为 O(n × n × 1),即 O(n^2)。
-
对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。
1 2 3 4 5 6 7 8 9 10 11 12
void aFunc(int n) { // 第一部分时间复杂度为 O(n^2) for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { printf("Hello, World!\n"); } } // 第二部分时间复杂度为 O(n) for(int j = 0; j < n; j++) { printf("Hello, World!\n"); } }
此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。
-
对于条件判断语句,总的时间复杂度等于其中 时间复杂度最大的路径 的时间复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
void aFunc(int n) { if (n >= 0) { // 第一条路径时间复杂度为 O(n^2) for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { printf("输入数据大于等于零\n"); } } } else { // 第二条路径时间复杂度为 O(n) for(int j = 0; j < n; j++) { printf("输入数据小于零\n"); } } }
此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。
常见的时间复杂度
- O(1)—常数阶:最低的时空复杂度,也就是耗时与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标。
- O(n)—线性阶,就代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。
- O(n^2)—平方阶, 就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n x n)的算法,对n个数排序,需要扫描n x n次。
- O(logn)—对数阶,当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。
- O(nlogn)—线性对数阶,就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)的时间复杂度。
o(logn)
我们都知道二分查找在最坏的情况下依次是n/2,n/4,n/8。。。。一直到1为止。
然后,意思就是要循环多少次才能查找到目标数呢,我们假设是x次。
然后我们可以观察到分母是每次都乘以1/2,分子不变,所以可以根据题意列出下面等式:
n*(1/2)^x = 1
运算一下,就是:2^x = n
再运算一下,就是::log2(n) = x
最后对数函数的底数省略掉,也就是:log(n) = x
常见时间复杂度大小排序
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)
o(logn), o(nlogn), 这里的 log 是以 2 为底的
如果 ,即 a的x次方(或a的幂函数) 等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作 。其中,a叫做对数的底数,N叫做真数,x叫做“以a为底N的对数”。
Last but not least
时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。