我不得不同意,当你第一次看到 O(log n) 算法时,这很奇怪……这个对数到底是从哪里来的?然而,事实证明,有几种不同的方法可以让日志项以大 O 表示法显示。这里有几个:
反复除以一个常数
取任意数n;比如说 16. 在得到小于或等于 1 的数之前,你可以将 n 除以多少次?对于 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。如果我们想求解 i 的值,该值最多为 1,我们得到
n / 2 i ≤ 1
n ≤ 2我
日志2 n ≤ i
换句话说,如果我们选择一个整数 i 使得 i ≥ log 2 n,那么在将 n 除以 i 次后,我们将得到一个最多为 1 的值。可以保证的最小 i 大约是 log 2 n,所以如果我们有一个算法除以 2 直到数字变得足够小,那么我们可以说它以 O(log n) 步结束。
一个重要的细节是,你将 n 除以什么常数并不重要(只要它大于一);如果除以常数 k,则需要 log k n 步才能达到 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) 次超出数组元素。
遵循分而治之的一般技术(将问题分成几部分,解决这些部分,然后将问题重新组合在一起)的算法往往在其中包含对数项,出于同样的原因 - 你不能继续切割一些对象超过 O(log n) 次的一半。您可能希望将归并排序视为一个很好的例子。
一次处理一位数字
以 10 为基数的 n 有多少位?好吧,如果数字中有 k 位,那么我们会认为最大的数字是 10 k的某个倍数。最大的 k 位数字是 999...9,k 次,这等于 10 k + 1 - 1。因此,如果我们知道 n 中有 k 位,那么我们知道 n 的值是最多 10 k + 1 - 1。如果我们想用 n 求解 k,我们得到
n ≤ 10 k+1 - 1
n + 1 ≤ 10 k+1
日志10 (n + 1) ≤ k + 1
(log 10 (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) 项。一个经典的例子是基数排序,它一次对整数进行一位数排序。基数排序有很多种,但它们通常运行时间为 O(n log U),其中 U 是被排序的最大可能整数。这样做的原因是,每次排序都需要 O(n) 时间,并且总共需要 O(log U) 次迭代来处理被排序的最大数字的每个 O(log U) 位。许多高级算法,例如Gabow 的最短路径算法或Ford-Fulkerson 最大流算法的缩放版本,在其复杂性中都有一个对数项,因为它们一次只工作一个数字。
至于关于如何解决该问题的第二个问题,您可能需要查看这个探索更高级应用程序的相关问题。鉴于此处描述的问题的一般结构,当您知道结果中有对数项时,您现在可以更好地了解如何思考问题,因此我建议您在给出答案之前不要查看答案一些想法。
希望这可以帮助!