【问题标题】:How to calculate order (big O) for more complex algorithms (eg quicksort)如何计算更复杂算法的顺序(大 O)(例如快速排序)
【发布时间】:2010-04-12 23:37:12
【问题描述】:

我知道有很多关于大 O 表示法的问题,我已经查过了:

仅举几例。

我通过“直觉”知道如何为nn^2n! 等计算它,但是我完全不知道如何为 log n、@ 的算法计算它987654329@、n log log n等等。

我的意思是,我知道快速排序是n log n(平均).. 但是,为什么?合并/梳理等也是一样。

谁能用不太数学的方式解释一下你是如何计算这个的?

主要原因是我即将进行一次大型面试,我很确定他们会要求这种东西。我已经研究了几天,每个人似乎要么解释了为什么冒泡排序是 n^2,要么对Wikipedia 有不可读的解释(对我来说)

【问题讨论】:

    标签: algorithm complexity-theory big-o


    【解决方案1】:

    对数是取幂的逆运算。求幂的一个例子是当您在每一步中将项目数加倍时。因此,对数算法通常将每一步的项目数减半。比如二分查找就属于这一类。

    许多算法需要对数个大步骤,但每个大步骤都需要 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)。

    【讨论】:

    • 我真的很喜欢你的解释,它让我们很容易理解为什么以及如何识别它们,谢谢!
    【解决方案2】:

    您通常可以为每次运行时将空间/时间减半的算法声明 log n。一个很好的例子是任何二进制算法(例如,二进制搜索)。您选择左侧或右侧,然后将您正在搜索的空间缩小一半。重复做一半的模式是log n。

    【讨论】:

    • 是的。值得一提的是,在 CS 中 log 表示对数 base 2 而不是通常假设的 base 10。 log n 表示您必须将 2 提高到的数字才能达到 n。所以 log 8 是 3,log 16 是 4,等等...
    • 实际上这有点误导段错误。虽然 log 通常确实指的是 base 2,但就大 Oh 表示法而言,这并不重要。 O(log_2 (n) ) 等价于 O(log_k (n) ),因为 log_k (n) = log_k (2) * log_2 (n)。这只是基本对数公式变化的简化:log_k(a)/log_k(b) = log_b(a)。那么因为 log_k (2) 是一个常数,所以 big oh 显然是等价的。
    • @Segfault 言归正传,big-O 复杂度没有考虑常数因素,log_e 和 log_2 的区别只是常数因素。
    • 感谢各位的澄清。
    • 很好的答案。考虑到这篇文章被浏览了多少次,我很惊讶它没有得到更多的支持。
    【解决方案3】:

    对于某些算法,通过直觉获得运行时间的严格限制几乎是不可能的(例如,我认为我永远无法凭直觉知道 O(n log log n) 运行时间,我怀疑有人会永远期望你)。如果你能接触到CLRS Introduction to Algorithms text,你会发现对渐近符号的相当彻底的处理,它是适当的严格而不是完全不透明的。

    如果算法是递归的,一种简单的推导界限的方法是写出一个递归,然后着手解决它,要么迭代,要么使用Master Theorem 或其他方式。例如,如果您不希望对此非常严格,获得快速排序运行时间的最简单方法是通过主定理——快速排序需要将数组划分为两个相对相等的子数组(应该相当直观地看到这是O(n)),然后在这两个子数组上递归调用 QuickSort。那么如果我们让T(n)表示运行时间,我们就有T(n) = 2T(n/2) + O(n),在Master Method中是O(n log n)

    【讨论】:

    • 《大白书》+1。 (是的,即使它现在大部分是绿色的,它也总是 TBWB。)
    • 其实最近才出第三版,所以现在大部分是蓝色的。
    【解决方案4】:

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

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

    O(log n) 通常意味着您可以在每次迭代中将数据集减半(例如二分查找)

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

    我很确定 'O(n log log n)' 没有任何意义。或者如果是这样,它会简化为 O(n log n)。

    【讨论】:

    • :D 我很漂亮,超短的“通常意味着”sn-p 对于在现场快速“分析”n log log n 非常有用......我还没有真正看到那些算法,但我在搜索时偶然发现了这个顺序.. 显然 han's 和 thorup's 是 en.wikipedia.org/wiki/Sorting_algorithm 的一个例子
    • O(n log log n) 算法确实存在。例如:portal.acm.org/citation.cfm?id=975984
    • O(n log log n) 算法通常简化为 O(n),因为 log log n 非常小 - 例如 log log (2^64) = 6。跨度>
    • @Niki 虽然 log log n 术语的速度影响很小,但将 O(n log log n) 算法描述为 O(n) 是不正确的。这就像说 3 对于足够大的值 3 等于 4。
    • 是的,我并不是说它们相等,只是在大多数情况下,O(loglogn) 可以被视为 O(1)。从 N=2^32 到 N=2^64 将 loglogn 从 5 增加到 6。要达到 7,您必须将 N 增加到 2^128。这种增长是如此缓慢,以至于看起来很平淡。出于学术研究和一般知识的目的,您当然是对的,但在应用中通常会忽略 loglogn 因素。
    【解决方案5】:

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

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

    考虑一下 Mergesort 的行为方式,我们将取出一个列表并将其拆分,然后取出其中的一半并再次拆分它,直到我们有 n 个长度为 1 的分区。我希望很容易看到在我们将列表拆分为 n 个分区之前,此递归只会进行 log (n) 深。

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

    现在考虑这个过程将花费的时间量,我们将有 log (n) 个级别,并且在每个级别我们都必须合并所有列表。事实证明,每个级别都需要 n 时间来合并,因为我们每次将合并总共 n 个元素。然后你可以很容易地看到,如果你把比较操作作为最重要的操作,那么使用 mergesort 对数组进行排序需要 n log (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 时间,因为合并是一个线性操作。如果递归有 log(n) 个级别,那么我们将执行这个线性操作 log(n) 次,因此我们的运行时间将是 n log(n)。

    对不起,如果这也没有帮助。

    【讨论】:

    • 谢谢,我喜欢你的解释,因为它为我提供了对其他类型的算法执行此操作的方法。但是我在这一部分:然后你可以很容易地看到它需要 n log (n)如果您将比较操作视为最重要的操作,那么是时候使用 mergesort 对数组进行排序了。这对我来说并不容易看到..但它补充了其他人的回答(当你必须对每个 n 元素应用 log(n) 操作时)......这就是你“很容易看到”它变成的方式n*登录?
    【解决方案6】:

    当应用分治算法时,将问题划分为子问题,直到它变得简单到微不足道,如果划分顺利,每个子问题的大小为 n/2 左右.这通常是大 O 复杂度中出现的 log(n) 的起源:O(log(n)) 是分区顺利时所需的递归调用数。

    【讨论】:

    • 是的,但是分治算法通常是 n log(n),因为当您将问题分成越来越小的位时,通常需要在分区长度上花费 n 时间的操作在每一步执行。
    • @Liberalkid - 我认为这不对。每次迭代中完成的工作通常与n 无关,它只是(或多或少)恒定的处理量。由于在 Big-O 表示法中忽略了常量,因此像二进制搜索这样的分治算法是 O(log n)。
    • 二分搜索并不是真正的分而治之,它更像是所谓的减少和征服。 Mergesort、Quicksort、FFT 等算法是分而治之的。除非您将问题分解为更小的子问题并解决这些问题,然后使用这些解决方案来解决更大的问题,否则这并不是真正的分而治之。
    • @Liberalkid, @keithjgrant:一些教科书包含一个单独的“减少和征服”类别。其他教科书将这两种算法归为“分而治之”类别。
    猜你喜欢
    • 2013-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-31
    相关资源
    最近更新 更多