您如何证明或说明快速合并排序是一种不稳定的算法?

时间:2019-06-14 06:10:50

标签: java algorithm mergesort array-algorithms

当我阅读第四版算法第二章的问题2.2.10时,一个问题使我感到困惑。这本书说快速合并算法的结果不稳定,我找不到证据,请帮帮我,谢谢!

public static void sort(Comparable[] a, int lo, int hi){
    if hi <= lo {
    return;
    }
    int mid = lo + (hi - lo) / 2;
    sort(a, lo, mid);
    sort(a, mid+1, hi);
    merge(a, lo, mid, hi);
}

// Why is the result of this sort not stable
private static void merge(Comparable[] a, int lo, int mid, int hi) { 
   for (int i = lo; i <= mid; i++)
      aux[i] = a[i]; 

   for (int j = mid+1; j <= hi; j++)
      aux[j] = a[hi-j+mid+1];

   int i = lo, j = hi; 
   for (int k = lo; k <= hi; k++) 
      if (less(aux[j], aux[i])) a[k] = aux[j--];
      else                      a[k] = aux[i++];
}

我找不到不稳定的结果,我该怎么办?

3 个答案:

答案 0 :(得分:4)

将“相等”元素保持相同顺序的排序算法被认为是稳定的。因此,不稳定的意思是:您有多个相等的元素,并且在对整个列表/数组进行排序时,排序的输出中有(可能)这些 equal 元素出现在不同的顺序。

例如,假设您有一个Person类,并且实现了相等性,以便仅查看姓氏,而忽略名字。

现在,假设您有两个Person对象,分别代表“ John Doe”和“ Jane Doe”。它们按此顺序在您的未排序列表中。

稳定表示:您总是以“ John Doe”出现在“ Jane Doe”之前。对于不稳定的排序,您将无法保证。

换句话说:您需要创建一个至少具有两个属性的类。然后,您需要定义compareTo()以仅依赖两个属性之一。

然后,创建该类对象的一些示例列表,然后进行足够长的试验,直到找到一个示例,其中排序后的列表显示相等的对象更改了顺序。

换句话说:创建一个列表(p1,p2,p3,p4,...),对其进行排序,然后寻找可能表示... p4,p3 ...(尽管p4和p3)的结果被视为“相等”。

最后:对于使用某些基于基于属性的测试框架(例如QuickCheck),这实际上是一个非常不错的用例。使用这样的框架,您将需要:

  • 创建一个“生成器”,该生成器可以创建稍后要排序的某些类的“随机”对象(在其中倾斜生成器以确保从中获得一堆“相等”对象)
  • 然后让框架测试基本的“断言”,即排序之前和之后“相等”对象的顺序不得更改。

然后让框架发挥作用……

答案 1 :(得分:1)

为证明算法的稳定性,仅需一个反例即可:让我们考虑一下对由A B C D谓词比较相等的4个元素less组成的数组进行排序的步骤。

  • sort(a, 0, 3)在2个子数组上递归:
  • sort(a, 0, 1)再次递归
  • sort(a, 0, 0)立即返回
  • sort(a, 1, 1)立即返回
  • merge(a, 0, 0, 1)不会更改A B的顺序
  • sort(a, 2, 3)重复发生
  • sort(a, 2, 2)立即返回
  • sort(a, 3, 3)立即返回
  • merge(a, 2, 2, 3)不会更改C D的顺序
  • merge(a, 0, 1, 3)按照A B C D的顺序将项t复制到A B D C中,然后合并循环中的所有比较结果都为false,因此将元素复制回{{ 1}}的顺序相同,从a复制:t[i++],证明排序算法不稳定,即:比较比较的元素的相对顺序未保留。

答案 2 :(得分:0)

要证明排序算法不稳定,只需找到一个失败即可。证明排序算法是稳定的会涉及更多。检查失败的一种方法是使用整数数组并将整数分为两部分,高8位为伪随机值,低24位等于整数的索引(0到count-1)。然后运行排序,仅使用高8位进行比较,例如在C:

    if((b[j]&0xff000000) < (b[i]&0xff000000)) ...

排序完成后,请使用所有32位检查数组是否有序。

使用这种方法,我能够确认合并排序的这种变化是不稳定的。

显然,这被称为“快速”合并排序的原因是,执行合并时不检查运行结束。左游程从lo到中间按正序复制到aux []中,而右游程从hi到Mid + 1反向复制到aux []中。然后合并从两端开始(lo和hi),并向中间(mid和mid + 1)工作,使用 i 的左行从lo到中间向前移动,使用的右行向后运行> j 从hi到mid + 1。由于没有检查是否可以到达运行结束,因此 i 可能会增加到中点以上(潜在的稳定性问题),或者 j 可能会减少到中点+1以下(不是稳定性问题)。如果 i 增大到mid以上,并且aux [mid + 1] == aux [mid + 2](原始右行的两个最高元素),则会破坏稳定性。在这种情况下,将以相反的顺序复制元素。

尽管该书将其称为快速合并排序,但避免在aux中复制数据,而是根据递归级别更改合并方向会更快。对于自上而下的情况,可以使用一种类型的副本并在递归调用中交换数组引用来完成,例如以下Wiki示例:

https://en.wikipedia.org/wiki/Merge_sort#Top-down_implementation

可以使用一对相互递归的函数来避免初始副本,一个函数的结果以a []结尾,另一个函数的结果以b []结尾。

自底向上合并排序稍快一点,因为它跳过了所有递归拆分和索引在堆栈上的存储。在这种情况下,合并的方向基于合并过程。为了使通过次数保持均匀,可以预先检查奇数次通过次数,并在开始第一次自下而上的合并排序通过之前将成对的元素交换到位。