时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

了解O(logn)的意思

预先知道算法的复杂度是一回事,了解其后的原理是另一件事情。

不管你是计算机科班出身还是想有效解决最优化问题,如果想要用自己的知识解决实际问题,你都必须理解时间复杂度。

先从简单直观的 O(1) 和 O(n) 复杂度说起。O(1) 表示一次操作即可直接取得目标元素(比如字典或哈希表),O(n) 意味着先要检查 n 个元素来搜索目标,但是 O(log n) 是什么意思呢?

你第一次听说 O(log n) 时间复杂度可能是在学二分搜索算法的时候。二分搜索一定有某种行为使其时间复杂度为 log n。我们来看看是二分搜索是如何实现的。

因为在最好情况下二分搜索的时间复杂度是 O(1),最坏情况(平均情况)下 O(log n),我们直接来看最坏情况下的例子。已知有 16 个元素的有序数组。

举个最坏情况的例子,比如我们要找的是数字 13。

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

十六个元素的有序数组

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

选中间的元素作为中心点(长度的一半)

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

13 小于中心点,所以不用考虑数组的后一半

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

重复这个过程,每次都寻找子数组的中间元素

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

每次和中间元素比较都会使搜索范围减半。

所以为了从 16 个元素中找到目标元素,我们需要把数组平均分割 4 次,也就是说,

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

简化后的公式

类似的,如果有 n 个元素,

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

归纳一下

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

分子和分母代入指数

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

等式两边同时乘以 2^k

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

最终结果

现在来看看「对数」的定义:

为使某数(底数)等于一给定数而必须取的乘幂的幂指数。

也就是说可以写成这种形式

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

对数形式

所以 log n 的确是有意义的,不是吗?没有其他什么可以表示这种行为。

几种常见时间复杂度比较

做算法分析的时候经常用到各种时间复杂度如O(n), O(logn), O(nlogn), O(n^2), … 它们之间到底有多大的差别呢?下面这张图是一个直观的表达:

时间复杂度O(logn)你真的搞懂了吗?几种常见的复杂度比较

可见,各个常用的时间复杂度之间都存在着巨大的差异。从O(nlogn)到O(n),从O(n)到O(logn),都是性能上的巨大飞跃。

从另一个角度而言,大于$O(n^2) O(n^3) $时间复杂度的程序实际上都是不可用的。根据维基百科,现在最强的CPU每秒大概可执行428亿条指令4* 101010^{10} , 而对于一个O(2n)O(2^n) 的程序,当n=100时,就算有$2 ^ {100} $条指令,即$2 ^ {100} $ = 1.26765 * 103010^{30} 条指令。这样的CPU大概要算1万亿年(10^12)。

算法的运行时间通常与下列函数成比例:

1 大部分程序的大部分指令之执行一次,或者最多几次。如果一个程序的所有指令都具有这样的性质,我们说这个程序的执行时间是常数。
log log N 可以看作是一个常数:即使N很多,两次去对数之后也会变得很小
logN 如果一个程序的运行时间是对数级的,则随着N的增大程序会渐渐慢下来,如果一个程序将一个大的问题分解成一系列更小的问题,每一步都将问题的规模缩减成几分之一,一般就会出现这样的运行时间函数。在我们所关心的范围内,可以认为运行时间小于一个大的常数。对数的基数会影响这个常数,但改变不会太大:当N=1000时,如果基数是10,logN等于3;如果基数是2,logN约等于10.当N=1 00 000,logN只是前值的两倍。当N时原来的两倍,logN只增长了一个常数因子:仅当从N增长到N平方时,logN才会增长到原来的两倍。
N 如果程序的运行时间的线性的,很可能是这样的情况:对每个输入的元素都做了少量的处理。当N=1 000 000时,运行时间大概也就是这个数值;当N增长到原来的两倍时,运行时间大概也增长到原来的两倍。如果一个算法必须处理N个输入(或者产生N个输出),那么这种情况是最优的。
NlogN 如果某个算法将问题分解成更小的子问题,独立地解决各个子问题,最后将结果综合起来,运行时间一般就是NlogN。我们找不到一个更好的形容,就暂且将这样的算法运行时间叫做NlogN。当N=1 000 000时,NlogN大约是20 000 000。当N增长到原来的两倍,运行时间超过原来的两倍,但超过不是太多。
N平方 如果一个算法的运行时间是二次的(quadratic),那么它一般只能用于一些规模较小的问题。这样的运行时间通常存在于需要处理每一对输入数据项的算法(在程序中很可能表现为一个嵌套循环)中,当N=1000时,运行时间是1 000 000;如果N增长到原来的两倍,则运行时间将增长到原来的四倍。
N三次方 类似的,如果一个算法需要处理输入数据想的三元组(很可能表现为三重嵌套循环),其运行时间一般就是三次的,只能用于一些规模较小的问题。当N=100时,运行时间就是1 000 000;如果N增长到原来的两倍,运行时间将会增长到原来的八倍。
2的N次方 如果一个算法的运行时间是指数级的(exponential),一般它很难在实践中使用,即使这样的算法通常是对问题的直接求解。当N=20时,运行时间是1 000 000;如果增长到原来的两倍时,运行时间将是原时间的平方!