为什么合并排序最坏情况运行时间O(n log n)?

时间:2011-10-18 02:43:20

标签: algorithm mergesort

有人可以用简单的英语或简单的方式向我解释一下吗?

8 个答案:

答案 0 :(得分:63)

合并排序使用 Divide-and-Conquer 方法来解决排序问题。首先,它使用递归将输入分成两半。分割后,它对半部分进行排序并将它们合并为一个有序输出。见图

MergeSort recursion tree

这意味着首先对问题的一半进行排序并执行简单的合并子例程会更好。因此,了解merge子例程的复杂性以及在递归中调用它的次数非常重要。

合并排序的伪代码非常简单。

# C = output [length = N]
# A 1st sorted half [N/2]
# B 2nd sorted half [N/2]
i = j = 1
for k = 1 to n
    if A[i] < B[j]
        C[k] = A[i]
        i++
    else
        C[k] = B[j]
        j++

很容易看出,在每个循环中你将有4个操作: k ++ i ++ j ++ 如果声明归因C = A | B 。因此,您将具有小于或等于4N + 2个操作,从而产生 O(N)复杂度。为了证明,4N + 2将被视为6N,因为对于N = 1( 4N + 2 <= 6N )是正确的。

假设你有一个 N 元素的输入,并假设 N 是2的幂。在每个级别,你有两倍的子问题,输入有半个元素从以前的输入。这意味着在 j = 0,1,2,...,lgN级别会有 2 ^ j 子问题,输入长度 N / 2 ^ j 。每个级别 j 的操作次数将小于或等于

  

2 ^ j * 6(N / 2 ^ j)= 6N

观察到你的等级总是低于或等于6N的操作并不重要。

由于存在lgN + 1级别,因此复杂性为

  

O(6N *(lgN + 1))= O(6N * lgN + 6N)= O(n lgN)

参考文献:

答案 1 :(得分:30)

在“传统”合并排序中,每次传递数据都会使排序的子部分的大小加倍。在第一次传递之后,文件将被分类为长度为2的部分。第二次传球后,长度为四。然后八,十六等,直到文件的大小。

有必要将排序部分的大小加倍,直到有一个部分包含整个文件。将截面大小的lg(N)倍增达到文件大小,并且每次传递数据将花费与记录数量成比例的时间。

答案 2 :(得分:20)

这是因为无论是最坏情况还是普通情况,合并排序只是在每个阶段将数组分成两半,从而得到lg(n)分量,而另一个N分量来自在每个阶段进行的比较。因此,它的组合几乎变为O(nlg n)。无论是平均情况还是最坏情况,lg(n)因子总是存在。休息N因子取决于在两种情况下进行的比较所得到的比较。现在最糟糕的情况是在每个阶段对N个输入进行N次比较。所以它变成了O(nlg n)。

答案 3 :(得分:18)

将数组拆分为具有单个元素的阶段,即将其称为子列表,

  • 在每个阶段,我们将每个子列表的元素与其相邻的子列表进行比较。例如,[重用@Davi的图像 ]

    enter image description here

    1. 在第1阶段,每个元素与其相邻元素进行比较,因此进行n / 2比较。
    2. 在阶段-2,子列表的每个元素与其相邻子列表进行比较,因为每个子列表都被排序,这意味着两个子列表之间的最大比较数是&lt; =子列表的长度,即2(在阶段)因为子列表的长度增加了一倍,所以在阶段3的阶段3和阶段4进行了4次和4次比较。这意味着每个阶段的最大比较数=(子列表的长度*(子列表的数量/ 2))==&gt; N / 2
    3. 正如您所观察到的,舞台总数将为log(n) base 2 因此,总复杂度将是== (每个阶段的最大比较数*阶段数)== O((n / 2)* log(n))==&gt; O(n日志(n))的

答案 4 :(得分:7)

算法merge-sort在O(n log n)中对大小为n的序列S进行排序 时间,假设可以在O(1)时间内比较S的两个元素。enter image description here

答案 5 :(得分:4)

递归树将具有深度log(N),并且在该树的每个级别,您将执行合并N工作以合并两个已排序的数组。

合并已排序的数组

要合并两个已排序的数组A[1,5]B[3,4],您只需从头开始迭代,选择两个数组之间的最低元素并递增该数组的指针。当两个指针到达各自数组的末尾时,你就完成了。

[1,5] [3,4]  --> []
 ^     ^

[1,5] [3,4]  --> [1]
   ^   ^

[1,5] [3,4]  --> [1,3]
   ^     ^

[1,5] [3,4]  --> [1,3,4]
   ^      x

[1,5] [3,4]  --> [1,3,4,5]
    x     x

Runtime = O(A + B)

合并排序图

您的递归调用堆栈将如下所示。工作从底部叶节点开始,然后起泡。

beginning with [1,5,3,4], N = 4, depth k = log(4) = 2

  [1,5]    [3,4]     depth = k-1 (2^1 nodes) * (N/2^1 values to merge per node) == N
[1]  [5]  [3]  [4]   depth = k   (2^2 nodes) * (N/2^2 values to merge per node) == N

因此,您N在树中的每个k级别工作k = log(N)

N * k = N * log(N)

答案 6 :(得分:3)

许多其他答案都很棒,但我没有看到任何与“合并排序树”示例相关的 height depth 。这是另一种处理问题的方法,重点关注树。这是另一张图片来帮助解释: enter image description here

回顾一下:正如其他答案所指出的那样,我们知道合并序列的两个排序切片的工作是在线性时间内运行的(我们从主排序函数调用的合并辅助函数)。
现在看一下这棵树,在那里我们可以想到根的每个后代(除根之外)作为对排序函数的递归调用,让我们尝试评估我们在每个节点上花费多少时间...自切片以来序列和合并(两者一起)占线性时间,任何节点的运行时间相对于该节点序列的长度是线性的。

这是树深度的来源。如果n是原始序列的总大小,则任何节点处序列的大小为n / 2 i ,其中i是深度。如上图所示。将这与每个切片的线性工作量放在一起,我们为树中的每个节点都有一个O(n / 2 i )的运行时间。现在我们只需要为n个节点加总。一种方法是识别树中每个深度级别有2个 i 节点。所以对于任何级别,我们都有O(2 i * n / 2 i ),这是O(n),因为我们可以抵消2 i < / SUP>取值!如果每个深度都是O(n),我们只需将其乘以此二叉树的 height ,即logn。答案:O(nlogn)

参考:Data Structures and Algorithms in Python

答案 7 :(得分:2)

MergeSort算法需要三个步骤:

  1. 分割步骤计算子数组的中间位置,它需要恒定的时间O(1)。
  2. 征服步骤递归排序两个大约每个元素的两个子数组。
  3. 组合步骤在每次传递中合并总共n个元素,最多需要n次比较,因此需要O(n)。
  4. 该算法需要大约logn传递来对n个元素的数组进行排序,因此总时间复杂度为nlogn。