如何计算更复杂算法的顺序(大O)(例如快速排序)

时间:2010-04-12 23:37:13

标签: algorithm complexity-theory big-o

我知道有很多关于大O符号的问题,我已经检查过:

仅举几例。

我通过“直觉”知道如何计算nn^2n!等等,但我完全迷失了如何计算 log nn log nn log log n 等。

我的意思是,我知道快速排序是n log n(平均而言)..但是,为什么?合并/梳子等同样的东西。

有人能用不太合算的方式解释我,你怎么计算这个?

主要原因是我即将面试,我很确定他们会要求这种东西。我已经研究了几天了,似乎每个人都有一个解释为什么冒泡排序是n ^ 2或者Wikipedia

上的不可读解释(对我而言)

6 个答案:

答案 0 :(得分:40)

对数是取幂的逆运算。取幂的一个例子是当你将每一步的项目数加倍时。因此,对数算法通常将每一步的项目数减半。例如,二分搜索属于此类别。

许多算法需要对数个大步骤,但每个大步骤都需要O(n)个工作单元。 Mergesort属于这一类。

通常,您可以通过将它们视为平衡二叉树来识别这些类型的问题。例如,这里是合并排序:

 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)工作的大步骤均表示mergesort整体为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)。

答案 1 :(得分:14)

对于每次运行时空间/时间减半的算法,通常可以声明log n。这方面的一个很好的例子是任何二进制算法(例如,二进制搜索)。你可以向左或向右选择,然后将你正在搜索的空间对齐一半。反复做一半的模式是log n。

答案 2 :(得分:6)

对于某些算法,通过直觉获得运行时间的紧密限制几乎是不可能的(例如,我不认为我能够直觉O(n log log n)运行时间,我怀疑任何人都会期待你)。如果你能够掌握CLRS Introduction to Algorithms text,那么你会发现对渐近符号的一种非常彻底的处理,这种符号是严格的而不是完全不透明的。

如果算法是递归的,那么派生约束的一种简单方法是写出一个递归,然后设置为迭代或使用Master Theorem或其他方式解决它。例如,如果你不想对它过于严格,那么获得QuickSort运行时间的最简单方法是通过Master Theorem - QuickSort需要将数组分成两个相对相等的子阵列(它应该是相当直观的看到它这是O(n)),然后在这两个子数组上递归调用QuickSort。然后,如果我们让T(n)表示运行时间,我们会T(n) = 2T(n/2) + O(n),主方法为O(n log n)

答案 3 :(得分:4)

查看此处提供的“电话簿”示例:What is a plain English explanation of "Big O" notation?

请记住,Big-O完全是关于 scale :随着数据集的增长,此算法需要多少操作?

O(log n)通常意味着您可以在每次迭代时将数据集剪切一半(例如二进制搜索)

O(n log n)表示您正在为数据集中的每个项执行O(log n)操作

我很确定'O(n log log n)'没有任何意义。或者如果是,则将其简化为O(n log n)。

答案 4 :(得分:3)

我将尝试直观地分析为什么Mergesort是n log n,如果你能给我一个n log log n算法的例子,我也可以解决它。

Mergesort是一个排序示例,它通过重复拆分元素列表,直到只存在元素,然后将这些列表合并在一起。这些合并中的每一个中的主要操作是比较,并且每个合并最多需要n次比较,其中n是组合的两个列表的长度。从中你可以得出重现并轻松解决它,但我们将避免使用该方法。

相反考虑Mergesort将如何表现,我们将采用一个列表并将其拆分,然后再取出一半并再次拆分,直到我们有n个长度为1的分区。我希望很容易看到这个递归只会深入log(n),直到我们将列表拆分为n个分区。

既然我们已经需要合并这些n个分区中的每个分区,那么一旦合并这些分区,就需要合并下一个级别,直到我们再次有一个长度为n的列表。有关此过程http://en.wikipedia.org/wiki/File:Merge_sort_algorithm_diagram.svg的简单示例,请参阅维基百科的图形。

现在考虑这个过程将花费的时间,我们将有log(n)级别,并且在每个级别我们将必须合并所有列表。事实证明,每个级别都需要n次合并,因为我们每次都会合并n个元素。然后,如果将比较操作作为最重要的操作,您可以相当容易地看到使用mergesort对n(n)时间进行排序。

如果有什么不清楚或我在某处跳过请告诉我,我可以尝试更详细。

编辑第二个说明:

让我想想如果我能更好地解释这一点。

将问题分解为一堆较小的列表,然后对较小的列表进行排序和合并,直到您返回到现在已排序的原始列表。

当你解决问题时你有几个不同的大小级别首先你会有两个大小的列表:n / 2,n / 2然后在下一个级别你将有四个大小的列表:n / 4 ,n / 4,n / 4,n / 4在下一级你将有n / 8,n / 8,n / 8,n / 8,n / 8,n / 8,n / 8,n / 8这继续直到n / 2 ^ k等于1(每个细分是长度除以2的幂,并非所有长度都可以被4整除,所以它不会那么漂亮)。这被重复除以2并且可以在大多数log_2(n)次继续,因为2 ^(log_2(n))= n,所以再除以2将产生大小为零的列表。

现在需要注意的重要一点是,在每个级别我们都有n个元素,因此对于每个级别,合并将花费n次,因为merge是一个线性操作。如果有递归的log(n)级别,那么我们将执行此线性操作log(n)次,因此我们的运行时间将为n log(n)。

对不起,如果这也没用。

答案 5 :(得分:0)

当应用一个分而治之的算法,你将问题划分为子问题,直到它如此简单以至于微不足道,如果分区顺利,每个子问题的大小是n / 2或其左右。这通常是大{O}复杂性中出现的log(n)的起源:O(log(n))是分区顺利进行时所需的递归调用的数量。