为啥在 MergeSort 中使用 InsertionSort 而不是 Merge 平均更快?

Posted

技术标签:

【中文标题】为啥在 MergeSort 中使用 InsertionSort 而不是 Merge 平均更快?【英文标题】:Why using InsertionSort instead of Merge in MergeSort is averagely faster?为什么在 MergeSort 中使用 InsertionSort 而不是 Merge 平均更快? 【发布时间】:2014-01-02 15:49:47 【问题描述】:

最近,我对ShellSort算法的想法很着迷,在小子列表中简单地使用InsertionSort,最后对整个列表使用InsertionSort。

所以,我想为什么不将 MergeSortInsertionSort 结合使用(而不是使用 Merge() 函数,而是使用 InsertionSort)。由于 InsertionSort 擅长对部分排序的列表进行排序,而 MergeSort 的想法是将两个排序列表合并为一个排序列表。

然后,我测试了 MergeSort 与 merge() 函数和 MergeSort 仅具有 10,000,000 个随机值元素的数组的 InsertionSort()。事实证明,带有 InsertionSort() 的 MergeSort 的执行速度比带有 merge() 函数的 MergeSort 快几倍。由于提出准确的数学证明超出了我的能力,所以我来这里寻求逻辑原因。以下是我要确认的:

对于更大的数组,带有 merge() 函数的 MergeSort 是否会平均优于带有 InsertionSort() 的 MergeSort,反之亦然? 也许我的 MergeSort 和 merge() 函数效率低下。 在 MergeSort 中使用 ShellSort 代替 InsertionSort 会产生更快的性能吗? 既然 MergeSort 和 InsertionSort 不是一个坏主意,我相信已经有人发现了它。我想知道它是否有任何独特的算法名称。

以下是MergeSort()的实现

public static void MergeSort(int[] array)

    int[] aux = new int[array.Length];
    MergeSort(array, aux, 0, array.Length - 1);


public static void MergeSort(int[] array, int[] aux, int low, int high) 

    if (low >= high) return;

    int mid = (low + high) / 2;

    MergeSort(array, aux, low, mid);
    MergeSort(array, aux, mid + 1, high);

    Merge(array, aux, low, mid, high);


protected static void Merge(int[] array, int[] aux, int low, int mid, int high) 
    // copy into aux array
    for (int i = low; i <= high; i++) aux[i] = array[i];

    // merge
    int j = low, k = mid + 1;
    for (int o = low; o <= high; o++) 
        if (j > mid)
            array[o] = aux[k++];
        else if (k > high)
            array[o] = aux[j++];
        else if (aux[k] < aux[j])
            array[o] = aux[k++];
        else
            array[o] = aux[j++];
    

下面是 MergeSort 和 InsertionSort()

public static void MergeInsertionSort(int[] array) 

    MergeInsertionSort(array, 0, array.Length - 1);


public static void MergeInsertionSort(int[] array, int low, int high) 

    if (low >= high) return;
    if (low + 1 == high) 
        // sort two elements
        if (array[low] > array[high]) 
            int tmp = array[low];
            array[low] = array[high];
            array[high] = tmp;
        
     else 
        int mid = (low + high) / 2;

        MergeInsertionSort(array, low, mid);
        MergeInsertionSort(array, mid + 1, high);

        // do insertion sort
        for (int i = mid + 1, j; i <= high; i++) 
            int ins = array[low];

            // move the element into correct position
            for (j = i - 1; (j >= low) && (ins < array[j]); j--) 
                array[j + 1] = array[j];
            

            array[j + 1] = ins;
        
    


以下是可运行的代码,你可以在电脑上测试一下: http://pastebin.com/4nh7L3H9

【问题讨论】:

Is there ever a good reason to use Insertion Sort? 的可能副本(或足够接近) @Dukeling,除了大多数答案只说通常 QuickSort 和 MergeSort 切换到小数据的 InsertionSort 之外,它非常接近,但我的问题是即使数据较大,也要坚持使用 InsertionSort 并且性能仍然比使用 merge() 的 MergeSort 相当好并且可能更快; @Dukeling,如果 MergeSort() 与纯 InsertionSort 比 MergeSort() 与纯 merge() 更有希望,并且使用合并两个排序列表需要 N/2 额外空间。 MergeSort() 和 InsertionSort() 不会排序更明智的算法吗? 您可能不会在相同的数据上一个接一个地执行,是吗?如果是这样,则在您开始时数据已经排序,因此您看到的结果不会是意外的。另外,这是怎么回事 - if (low + 1 == high),在这两种情况下您没有以相同方式处理它的任何具体原因?此外,一个完整的可运行程序可能会有所帮助。 @Dukeling,首先,我创建了一个包含 10,000,000 个随机值元素的数组,称为A。然后,我将数组A 克隆到两个数组,分别称为BC。然后,我启动秒表,并在数组B 上执行 MergeInsertionSort,然后停止秒表。重置秒表,然后在数组C 上执行 MergeSort。所以基本上,它们对相同的数据进行排序。 【参考方案1】:

您根本不是在测试相同的东西。您的Merge 方法使用辅助数组,它所做的第一件事是在执行实际合并工作之前将初始数组复制到辅助数组。因此,每次调用 Merge 时,您最终所做的工作量是您的两倍。

您可以通过对arrayaux 进行一些智能交换来消除多余的副本。这在非递归实现中更容易处理,但在递归版本中是可能的。我会把它留作练习。

您的MergeInsertionSort 方法的运作方式大不相同。它根本没有进行合并。它只是拆分数组并在越来越大的范围内进行插入排序。

这个想法是使用插入排序,以便在范围较小时减少合并的开销。通常它看起来像这样:

public static void MergeSort(int[] array, int[] aux, int low, int high) 

    if (low >= high) return;

    if ((high - low) < MergeThreshold)
    
        // do insertion sort of the range here
    
    else
    
        int mid = (low + high) / 2;

        MergeSort(array, aux, low, mid);
        MergeSort(array, aux, mid + 1, high);

        Merge(array, aux, low, mid, high);
    

您将MergeThreshold 设置为您认为合适的“小范围”值。通常它在 5 到 20 的范围内,但您可能希望尝试不同的值和不同的类型(整数、字符串、复杂对象等)以获得一个好的全面数字。

【讨论】:

【参考方案2】:

这里的问题是你的插入排序被破坏了,使它运行得更快,但返回无意义的输出(所有元素看起来都是相同的数字)。以下是运行MergeInsertionSort 后的tmp 数组示例(数组大小更改为10):

1219739925
1219739925
1219739925
1219739925
1219739925
1219739925
1219739925
1219739925
1219739925
1275593566

这是你的代码:

// do insertion sort
for (int i = mid + 1, j; i <= high; i++) 
    int ins = array[low];

    // move the element into correct position
    for (j = i - 1; (j >= low) && (ins < array[j]); j--) 
        array[j + 1] = array[j];
    

    array[j + 1] = ins;

问题出在这一行

    int ins = array[low];

这应该是:

    int ins = array[i];

修复此问题后,您会发现 MergeSortMergeInsertionSort 效率更高(您必须减小数组大小,使其在合理的时间内运行)。


另一方面,我花了一段时间才弄清楚,因为我最初只是检查输出是否已排序(它是排序的,但它不是输入的排序版本)而不是实际上检查它是否是输入的排序版本。直到我查看输出才发现问题。

【讨论】:

以上是关于为啥在 MergeSort 中使用 InsertionSort 而不是 Merge 平均更快?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 MergeSort 函数会发生值限制?

为啥我无法在 PHP 和 MySQL 中获取最后一个插入 ID?

在JavaScript中构建mergesort时的无限循环

MergeSort归并排序和利用归并排序计算出数组中的逆序对

Collections.sort in JDK6:MergeSort

为啥归并排序不是动态编程