对数是取幂的逆运算。求幂的一个例子是当您在每一步中将项目数加倍时。因此,对数算法通常将每一步的项目数减半。比如二分查找就属于这一类。
许多算法需要对数个大步骤,但每个大步骤都需要 O(n) 个工作单元。合并排序属于这一类。
通常,您可以通过将这些问题可视化为平衡二叉树来识别这些问题。例如,这里是归并排序:
6 2 0 4 1 3 7 5
2 6 0 4 1 3 5 7
0 2 4 6 1 3 5 7
0 1 2 3 4 5 6 7
顶部是输入,作为树的叶子。该算法通过对上面的两个节点进行排序来创建一个新节点。我们知道平衡二叉树的高度是 O(log n),所以有 O(log n) 个大步。但是,创建每个新行需要 O(n) 的工作。 O(log n) 大步的 O(n) 工作每一步都意味着归并排序总体上是 O(n log n)。
通常,O(log n) 算法看起来像下面的函数。他们可以在每一步丢弃一半的数据。
def function(data, n):
if n <= constant:
return do_simple_case(data, n)
if some_condition():
function(data[:n/2], n / 2) # Recurse on first half of data
else:
function(data[n/2:], n - n / 2) # Recurse on second half of data
虽然 O(n log n) 算法看起来像下面的函数。他们还将数据分成两半,但他们需要考虑两半。
def function(data, n):
if n <= constant:
return do_simple_case(data, n)
part1 = function(data[n/2:], n / 2) # Recurse on first half of data
part2 = function(data[:n/2], n - n / 2) # Recurse on second half of data
return combine(part1, part2)
do_simple_case() 花费 O(1) 时间,而 combine() 花费不超过 O(n) 时间。
算法不需要将数据精确地分成两半。他们可以把它分成三分之一和三分之二,那很好。对于一般情况下的性能,将其平均分成两半就足够了(如 QuickSort)。只要在 (n/something) 和 (n - n/something) 的片段上完成递归,就可以了。如果将其分解为 (k) 和 (n-k),那么树的高度将为 O(n) 而不是 O(log n)。