我必须承认,第一次看到O(log n)算法时感觉很奇怪...这个对数从哪里来的呢?然而,事实证明,在大O符号表示法中,有几种不同的方法可以出现对数项。以下是其中几种:
反复除以常数
取任意数字n,比如16。你可以将n除以2多少次,直到得到一个小于或等于1的数字?对于16,我们有
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
请注意,这最终需要四个步骤才能完成。有趣的是,我们还有log
2 16 = 4。嗯...那128呢?
128 / 2 = 64
64 / 2 = 32
32 / 2 = 16
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
这需要七个步骤,log
2 128 = 7。这是巧合吗?不是!有一个很好的原因。假设我们将一个数n除以2 i次。那么我们得到数字n / 2
i。如果我们想解出这个值最多为1的i的值,我们得到:
n / 2
i ≤ 1
n ≤ 2
i
log
2 n ≤ i
换句话说,如果我们选择一个整数i,使得i ≥ log
2 n,则在将n除以一半i次后,我们会得到一个最多为1的值。保证这样的最小i大约是log
2 n,因此,如果我们有一个算法,它通过除以2来使数字变得足够小,那么我们可以说它在O(log n)步内终止。
重要的细节是,无论你用什么常数来除以n(只要它大于1),如果你除以常数k,它将需要log
kn步骤才能到达1。因此,任何反复将输入大小除以某个分数的算法都需要O(log n)次迭代才能终止。这些迭代可能需要很长时间,因此净运行时间不一定是O(log n),但步骤的数量将是对数级别的。
那么这个概念在哪里会用到呢?一个经典的例子是
二分查找,一种快速搜索排序数组中值的算法。该算法的工作原理如下:
如果数组为空,则返回该元素不在数组中。
否则:
查看数组的中间元素。
如果它等于我们要查找的元素,则返回成功。
如果它大于我们要查找的元素:
丢弃数组的后半部分。
重复上述步骤。
如果它小于我们要查找的元素:
丢弃数组的前半部分。
重复上述步骤。
例如,在数组中搜索5。
1 3 5 7 9 11 13
我们首先看中间元素:
1 3 5 7 9 11 13
^
由于7>5,而且数组已经排序,我们可以确定数字5不可能在数组的后半部分,因此我们可以将其舍弃。这就剩下了:
1 3 5
现在我们来看这里的中间元素:
1 3 5
^
由于3小于5,我们知道5不可能出现在数组的前半部分,因此我们可以舍弃前半部分的数组。
5
再次,我们看一下这个数组的中间位置:
5
^
由于这正是我们要找的数字,因此我们可以报告5确实在数组中。
那么这有多有效呢?在每次迭代中,我们都会丢弃至少一半剩余的数组元素。算法会在数组为空或找到所需值时立即停止。在最坏的情况下,元素不存在,因此我们会将数组大小减半,直到元素用尽。这需要多长时间呢?由于我们一遍又一遍地将数组减半,因此我们最多需要O(log n)次迭代,因为在用尽数组元素之前,我们无法将数组减半超过O(log n)次。
遵循divide-and-conquer(将问题分成若干部分、解决这些部分,然后将问题重新组合)的通用技术的算法出于同样的原因,往往具有对数项 - 你不能将某个对象切成超过O(log n)次。您可能想看看merge sort,它是一个很好的例子。
逐位处理数值
一个十进制数n有多少个数字呢?如果这个数有k个数字,那么最大的数字是10的k次幂的某个倍数。最大的k位数是999...9,有k个9,等于10的k+1次幂减1。因此,如果我们知道n有k个数字,那么我们知道n的值最多为10的k+1次幂减1。如果我们想要解出k关于n的式子,我们得到:
n ≤ 10k+1 - 1
n + 1 ≤ 10k+1
log10 (n + 1) ≤ k + 1
(log10 (n + 1)) - 1 ≤ k
由此我们得出k大约是n的以10为底的对数。换句话说,n的位数是O(log n)。
例如,让我们考虑添加两个太大而无法适应机器字的大数字的复杂性。假设我们用十进制表示这些数字,并将这些数字称为m和n。一种加法方法是使用小学方法-逐位书写数字,然后从右到左进行计算。例如,要添加1337和2065,我们将首先将数字写出来。
1 3 3 7
+ 2 0 6 5
==============
我们将最后一位数字加上1并进位:
1
1 3 3 7
+ 2 0 6 5
==============
2
然后我们加上倒数第二个(“次后”)数字,并进位1:
1 1
1 3 3 7
+ 2 0 6 5
==============
0 2
接下来,我们添加倒数第三位(“antepenultimate”)数字:
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
最后,我们添加倒数第四位(“preantepenultimate”...我喜欢英语)的数字:
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
现在,我们做了多少工作?对于每个数字,我们总共进行O(1)的工作量(也就是说,一个恒定的工作量),需要处理的数字总数为O(max {log n,log m})。因为我们需要访问两个数字中的每个数字,所以总复杂度为O(max {log n,log m})。请注意保留html标记。
许多算法在某些基数中每次处理一位数字,因此它们的复杂度中会有一个O(log n)项。一个经典的例子是
基数排序,它逐位对整数进行排序。基数排序有许多不同的变体,但通常的时间复杂度为O(n log U),其中U是被排序的最大可能整数。这是因为排序的每个步骤需要O(n)的时间,并且总共需要O(log U)次迭代来处理被排序的最大数字的O(log U)位数字。许多高级算法,例如
Gabow的最短路径算法或
Ford-Fulkerson最大流算法的比例版本,其复杂度中有一个log项,因为它们逐位处理数字。
关于你的第二个问题,如何解决这个问题,你可能想看看
这个相关的问题,它探讨了更高级的应用。鉴于这里描述的问题的一般结构,当你知道结果中有一个对数项时,现在你可以更好地思考问题的方式,所以我建议在考虑清楚之前不要看答案。
O(log n)
可以理解为:如果您将问题规模n
增加一倍,您的算法只需要增加常数步骤即可。 - phimuemue