我不得不承认,当你第一次看到 O(log n) 算法时,这很奇怪……这个对数到底是从哪里来的?然而,事实证明,有几种不同的方法可以让日志项以大 O 表示法显示。以下是一些:
反复除以常数
取任意数n;比如说 16. 在得到小于或等于 1 的数之前,你可以将 n 除以多少次?对于 16,我们有这个
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
请注意,这最终需要四个步骤才能完成。有趣的是,我们也有 log2 16 = 4。嗯……那么 128 呢?
128 / 2 = 64
64 / 2 = 32
32 / 2 = 16
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
这需要七个步骤,并且 log2 128 = 7。这是巧合吗?没有!这是有充分理由的。假设我们将一个数 n 除以 2 i 次。然后我们得到数字 n / 2i。如果我们要求解 i 的值,该值最多为 1,我们得到
n / 2i ≤ 1
n ≤ 2i
log2 n ≤ i
换句话说,如果我们选择一个整数 i 使得 i ≥ log2 n,那么在将 n 除以 i 次后,我们将得到一个最多为 1 的值。最小的可以保证的 i 大约是 log2 n,所以如果我们有一个算法除以 2 直到数字变得足够小,那么我们可以说它以 O(log n) 步结束.
一个重要的细节是,你将 n 除以什么常数并不重要(只要它大于一);如果除以常数 k,则需要 logk n 步才能达到 1。因此,任何将输入大小重复除以某个分数的算法都需要 O(log n) 次迭代才能终止。这些迭代可能会花费大量时间,因此净运行时间不必为 O(log n),但步数将是对数。
那么这在哪里出现?一个经典的例子是 binary search,这是一种快速算法,用于在已排序的数组中搜索值。该算法的工作原理如下:
- 如果数组为空,则返回该元素不存在于数组中。
- 否则:
- 查看数组的中间元素。
- 如果它等于我们要查找的元素,则返回成功。
- 如果它大于我们要查找的元素:
- 如果它小于我们要查找的元素:
例如在数组中搜索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 确实在数组中。
那么效率如何?好吧,在每次迭代中,我们都会丢弃至少一半的剩余数组元素。一旦数组为空或者我们找到我们想要的值,算法就会停止。在最坏的情况下,元素不存在,所以我们一直将数组的大小减半,直到我们用完元素。这需要多长时间?好吧,由于我们一遍又一遍地把数组切成两半,我们最多会在 O(log n) 次迭代中完成,因为在运行之前我们不能将数组切成两半超过 O(log n) 次超出数组元素。
遵循 divide-and-conquer 的一般技术的算法(将问题分成几部分,解决这些部分,然后将问题重新组合在一起)出于同样的原因,它们中往往包含对数项 - 你不能将某些物体切割成 O(log n) 次以上。您可能希望将 merge sort 视为一个很好的例子。
一次处理一个数字
以 10 为底的数字 n 有多少位?好吧,如果数字中有 k 位,那么我们会认为最大的数字是 10k 的某个倍数。最大的 k 位数是 999...9,k 次,这等于 10k + 1 - 1。因此,如果我们知道 n 中有 k 位数,那么我们知道 n 的值最多为 10k + 1 - 1。如果我们想用 n 来求解 k,我们得到
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)。
例如,让我们考虑将两个太大而无法放入机器字中的大数字相加的复杂性。假设我们有以 10 为基数的数字,我们将数字称为 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
接下来,我们添加倒数第三个(“倒数第二个”)数字:
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}) 的复杂度,因为我们需要访问两个数字中的每个数字。
许多算法通过在某个基数中一次处理一个数字来获得 O(log n) 项。一个经典的例子是 radix sort,它一次将整数排序一位。基数排序有很多种,但它们通常运行时间为 O(n log U),其中 U 是被排序的最大可能整数。这样做的原因是每次排序都需要 O(n) 时间,并且总共需要 O(log U) 次迭代来处理被排序的最大数字的每个 O(log U) 位。许多高级算法,例如Gabow's shortest-paths algorithm 或Ford-Fulkerson max-flow algorithm 的缩放版本,在其复杂性中都有一个对数项,因为它们一次只工作一个数字。
关于您如何解决该问题的第二个问题,您可能需要查看this related question,它探索了一个更高级的应用程序。鉴于此处描述的问题的一般结构,当您知道结果中有一个对数项时,您现在可以更好地了解如何思考问题,因此我建议您在给出答案之前不要查看答案一些想法。
希望这会有所帮助!