Algorithm

算法复杂度

2017-10-11  本文已影响42人  几千里也

时间复杂度

一个算法花费的时间,与算法中语句的执行次数成正比例。哪个算法中语句执行次数多,它花费时间就多。我们把一个算法中的语句执行次数称为 语句频度时间频度,记为 T(n)。这里的 n 表示问题的规模。当 n 不断变化时,时间频度 T(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)) 为算法的渐进时间复杂度,简称时间复杂度。

空间复杂度

空间复杂度是指算法在计算机内执行时所需存储空间的度量,与时间复杂度类似,记作 S(n)=O(f(n))。
算法执行期间所需要的存储空间包括 3 个部分:

Big O notation

大 O 符号是一种算法复杂度的相对表示方式。这个句子里有一些重要而严谨的用词:

123456789012 这两个数字来举例,我们在学校里学到的基本算术操作是:

它们中每一个都是一次操作或一个问题。为它们求解的方法就被叫做
算法algorithm)。

加法是最简单的了。你把加数排成行,按列加上每个数字,把所加得的数的末位数字写到结果里。所加得的数的十位及其以上的数字转入下一列的计算中。

让我们假设在算法中,加上这些数是计算开销最大的操作。合乎情理的说,为了把这两个数加起来我们必须要加 6 次数字(并且可能进位到第 7 次)。如果我们把两个 100 位数相加,我们必须做 100 次加法操作。如果我们把两个 10,000 位数相加,我们必须做 10,000 次加法操作。

看到这里的模式了吗?复杂度complexity,就是操作的数量),对于加法中较大数的数字个数 n,是直接成比例的。我们称这为 O(n) 或者 线性复杂度(linear complexity)

除了借位替代了进位,减法也是相似的。

乘法就不同了。你把乘数排成行,取放在下面的乘数的第 1 个数字,把它逆序乘以上面乘数的每一个数字。下面乘数的其余数字也这样做。所以为了乘我们的两个 6 位数乘数,我们必须做 36 次乘法操作。我们还需要做 10 或 11 次列的加法操作来得到最终结果。

如果我们有两个 100 位数相乘,我们需要做 10,000 次乘法操作和 200 次加法操作。两个 100 万位数相乘,我们需要做 1 万亿 (1012) 次乘法操作和 200 万次加法操作。

作为 n 平方的算法衡量尺度,这就是 O(n2),即平方复杂度(quadratic complexity)。现在是时候介绍另一个重要概念了:

我们只关心复杂度最重要的部分

敏锐的人可能已意识到,我们可以把操作次数表示为:n2 + 2n。但正如你所看到的,我们的两个 100 万位数相乘的例子,第二个 2n 无关紧要(在那个阶段,2n 只占操作总量的 0.0002%)。

有人注意到我们在这里假设场景为最坏的情况。当我们做 6 位数乘法时,如果其中一个是 4 位数另一个是 6 位数,那么我们只需做 24 次乘法操作。然而,对于那个’n’,我们仍然计算最坏情况,即乘数都是 6 位数的情况。因此,大 O 符号是关于一个算法的最坏情况的。

电话簿例子

假设电话薄把人按这样的顺序排列:姓、缩写或名、地址、然后是电话号码。

现在,如果你要指示计算机在一个包含 1,000,000 个名字的电话簿中查找『史大佗』的电话号码,你会怎么做?忽略也许你能猜测出『史』从电话簿哪里开始的事实(假设你不能猜测),你会怎么做?

一种典型的实现也许是,打开电话簿的正中间,取第 500,000 条记录,把它和『史』进行比较。如果这恰好就是『史大佗』,那我们真幸运。然而,『史大佗』更有可能在其前面或后面。如果在后面,那么我们把电话簿后面一半从中间划分开,然后重复之前的过程;如果在前面,那么我们把第一半从中间划分开,然后重复之前的过程。以此类推。

这种算法叫做二分搜索(binary search)。不论你是否意识到,它在编程中每天都用到。

因此,如果你想要在包含 100 万名字的电话簿中查找一个名字,事实上,通过这种算法,最多 20 次,你能找到任何名字。在比较搜索算法中,我们决定把比较操作作为我们的 ’n’

这简直好得难以置信,不是吗?

用大 O 术语就是 O(log n),即对数复杂度(logarithmic complexity)。现在问题中的对数可以是 ln(底数为 e),log10,log2 或者以其它为底数,这无关紧要,它仍然是 O(log n),正如 O(2n2) 和 O(100n2) 都记为 O(n2)。

现在,值得花时间说明一下,对于算法,大 O 符号能够被用于决定 3 种情况:

通常我们不关心最好情况。我们对期望和最坏情况感兴趣。有时,期望情况更重要,有时最坏情况更重要。

回到电话簿的例子上来。

如果你有一个电话号码,想要查找名字,要怎么做呢?警察有一个相反(按电话号码排列)的电话簿,但是对于一般公众,这样的查询会被拒绝,是吧?技术上,你能在普通电话簿中查找一个号码。要怎么做呢?

你从第一个名字开始比较号码。如果吻合,很棒,如果不吻合,你移到下一条记录。你必须这样做,因为电话簿是无序(unordered)的(电话号码的排列是无序的)。

因此,查找一个名字:

旅行商例子

这是计算机科学中值得提到的一个相当有名的问题。在这个问题中,有 N 个城镇,每个城镇通过道路与 1 个或多个其它城镇相连,道路的路程是确定的。旅行商问题就是找出访问每个城镇的最短路线。

听起来很简单?再想想。

如果有 3 个城镇 A、B、C,两两之间都有道路,那么你可以这样走:

A -> B -> C
A -> C -> B
B -> C -> A
B -> A -> C
C -> A -> B
C -> B -> A

好吧,事实上,实际路线比上面的少,因为一些路线是等价的(例如,A -> B -> C 和 C -> B -> A 是等价的,因为它们使用同一条路线,只是方向相反)。所以,事实上,这里有 3 条可能的路径。

增加到 4 个城镇,你有 12 条可能的路径(如果我没记错)。5 个城镇,60 条可能的路径。6 个城镇,360 条可能的路径。

这是一个被叫做阶乘(factorial)的数学运算函数。大体上:

5! = 5 * 4 * 3 * 2 * 1 = 120
6! = 6 * 5 * 4 * 3 * 2 * 1 = 720
7! = 7 * 6 * 5 * 4 * 3 * 2 * 1 = 5040
…
25! = 25 * 24 * … * 2 * 1 = 15,511,210,043,330,985,984,000,000
…
50! = 50 * 49 * … * 2 * 1 = 3.04140932 * 1064

所以旅行商问题的大 O 符号表示是 O(n!),即阶乘(factorial)组合复杂度(combinatorial complexity)

当你有 200 个城镇的时候,使用传统计算机,那么全世界已经没有足够的时间来解决这个问题了。

上一篇下一篇

猜你喜欢

热点阅读