算法:混合归并排序和插入排序的执行时间

11

大家好,SO社区:

我是一名计算机科学专业的学生,目前正在进行一项实验,将MergeSort和InsertionSort相结合。我们知道,在特定阈值S下,InsertionSort的执行时间比MergeSort更快。因此,通过合并这两种排序算法,可以优化总运行时间。

然而,经过多次实验,使用1000个样本大小和不同大小的S,实验结果并没有每次给出明确的答案。下面是得到的较好结果的图片(请注意,有一半时间结果并不那么明确):

Execution Time for varying sizes of S, with sample size of 1000. Original Mergesort vs Hybrid Mergesort

现在,我们尝试使用3500个样本大小来运行相同的算法代码:

enter image description here

最后,我们尝试使用500,000个样本大小的相同算法代码(请注意,y轴以毫秒为单位):

enter image description here

尽管从逻辑上讲,当S<=10时,混合MergeSort将更快,因为InsertionSort没有递归开销时间。然而,我的小实验结果表明不是这样的。

目前,这些是教给我的时间复杂度:

MergeSort:O(n log n)

InsertionSort:

  • 最好情况:θ(n)
  • 最坏情况:θ(n^2)

最后,我在网上找到了一个资源:https://cs.stackexchange.com/questions/68179/combining-merge-sort-and-insertion-sort它指出:

Hybrid MergeInsertionSort:

  • 最好情况:θ(n + n log (n/x))
  • 最坏情况:θ(nx + n log (n/x))

我想问一下,是否有CS社区的结果显示,在某个阈值S下,混合的MergeSort算法将比普通的MergeSort算法效果更好,并且为什么?

非常感谢SO社区,这可能是一个琐碎的问题,但它确实会澄清我目前对时间复杂度和其他方面的许多疑问。

注意:我正在使用Java来编写算法,并且运行时间可能会受到java存储数据的方式的影响。

Java代码:

 public static int mergeSort2(int n, int m, int s, int[] arr){
        int mid = (n+m)/2, right=0, left=0;
        if(m-n<=s)
            return insertSort(arr,n,m);
        else
        {
            right = mergeSort2(n, mid,s, arr);
            left = mergeSort2(mid+1,m,s, arr);
            return right+left+merge(n,m,s,arr);
        }      
    }

    public static int insertSort(int[] arr, int n, int m){
        int temp, comp=0;
        for(int i=n+1; i<= m; i++){
            for(int j=i; j>n; j--){ 
                comp++;
                comparison2++;
                if(arr[j]<arr[j-1]){
                    temp = arr[j];
                    arr[j] = arr[j-1];
                    arr[j-1] = temp;
                }
                else
                    break;
            }
        }
        return comp;
    }

    public static void shiftArr(int start, int m, int[] arr){
        for(int i=m; i>start; i--)
            arr[i] = arr[i-1];     
    }

public static int merge(int n, int m, int s, int[] arr){
        int comp=0;
        if(m-n<=s)
            return 0;
        int mid = (n+m)/2;
        int temp, i=n,  j=mid+1;
        while(i<=mid && j<=m)
        {
            comp++;
            comparison2++;


            if(arr[i] >= arr[j])
            {
                if(i==mid++&&j==m && (arr[i]==arr[j]))
                    break;
                temp = arr[j];
                shiftArr(i,j++,arr);
                arr[i] = temp;
                if(arr[i+1]==arr[i]){
                   i++;
                }
            }
            i++;


        }
        return comp;
    }

有趣的工作!我不会评论这是否是一个适合在 Stack Overflow 上发布的好问题,但我建议您也在计算机科学堆栈交换上发布它,以获得更多关注。 - Tyler
嗨@Tyler,好的,我会的。它说我还需要等待另外20分钟才能在CS Stack Exchange上发布它 :) - Benji Tan
2
3500个元素不足以显示渐近运行时间。另外请附上您的代码,Java很容易创建有缺陷的基准测试。 - Thomas Jungblut
嗨@ThomasJungblut!我已经包含了代码,但很遗憾我是SO的新手,不知道如何创建fiddle..目前正尝试用样本量为500,000来计算结果:) - Benji Tan
1
嗨,@Tyler,请不要鼓励人们在多个 SE 网站上交叉发布。每个社区都应该有一个公正的机会回答问题,而不浪费任何人的时间。谢谢! - D.W.
1个回答

5

示例代码不是传统的归并排序。合并函数将一个数组移动而不是在原始数组和临时工作数组之间合并运行。

我测试了自顶向下和自底向上的归并排序,对于排序500,000个32位整数,两者都需要约42毫秒== 0.042秒,而图表中显示的结果大约慢了1000倍,需要约42秒而不是42毫秒。我还测试了10,000,000个整数,它需要略微超过1秒进行排序。

过去,使用C++,我比较了自底向上的归并排序和混合自底向上归并/插入排序,并且针对1600万(2 ^ 24 == 16,777,216)个32位整数,使用S == 16,混合排序比自底向上归并排序快约8%。 S == 64比S == 16稍慢。Visual Studio std :: stable_sort是自底向上归并排序的变体(临时数组的大小为原始数组的1/2)和插入排序,并且使用S == 32。

对于小数组,插入排序比归并排序更快,因为插入排序在排序小数组时具有更好的高速缓存局部性和所需指令数量更少的组合。对于伪随机数据和S == 16到64,插入排序比归并排序快约两倍。

随着数组大小的增加,相对收益会减少。考虑自底向上归并排序的影响,使用S == 16,仅优化了4次归并操作。在我的测试用例中,2 ^ 24 == 16,777,216个元素,即4/24 = 1/6 ~= 16.7%的操作数,导致约8%的改进(因此,在这4个操作中插入排序大约比归并排序快两倍)。总时间约为合并排序1.52秒,混合排序约为1.40秒,可以获得0.12秒的收益,而整个过程只需要1.52秒。对于自顶向下的归并排序,使用S == 16,将优化最深的4级递归。

更新-示例Java代码用于具有O(n log(n))时间复杂度的基于混合就地归并排序/插入排序。(注意:由于递归,辅助存储仍然在堆栈上消耗。)通过在合并步骤期间交换已合并到的区域中的数据和从中合并的数据来实现就地部分。这不是一个稳定的排序(由于合并步骤期间的交换而无法保留相等元素的顺序)。对于排序500,000个整数,大约需要1/8秒,因此我将其增加到1600万(2 ^ 24 == 16777216)个整数,需要略微超过4秒。如果没有插入排序,则需要约4.524秒进行排序,并且使用S == 64的插入排序,则需要约4.150秒,即约8.8%的收益。在C中使用基本相同的代码时,改进较小:从2.88秒到2.75秒,即约4.5%。

package msortih;
import java.util.Random;

public class msortih {

    static final int S = 64;    // use insertion sort if size <= S

    static void swap(int[] a, int i, int j) {
        int tmp = a[i]; a[i] = a[j]; a[j] = tmp;
    }

    // a[w:] = merged a[i:m]+a[j:n]
    // a[i:] = reordered a[w:]
    static void wmerge(int[] a, int i, int m, int j, int n, int w) {
        while (i < m && j < n)
            swap(a, w++, a[i] < a[j] ? i++ : j++);
        while (i < m)
            swap(a, w++, i++);
        while (j < n)
            swap(a, w++, j++);
    }  

    // a[w:]  = sorted    a[b:e]
    // a[b:e] = reordered a[w:]
    static void wsort(int[] a, int b, int e, int w) {
        int m;
        if (e - b > 1) {
            m = b + (e - b) / 2;
            imsort(a, b, m);
            imsort(a, m, e);
            wmerge(a, b, m, m, e, w);
        }
        else
            while (b < e)
                swap(a, b++, w++);
    }

    // inplace merge sort a[b:e]
    static void imsort(int[] a, int b, int e) {
        int m, n, w, x;
        int t;
        // if <= S elements, use insertion sort
        if (e - b <= S){
            for(n = b+1; n < e; n++){
               t = a[n];
               m = n-1;
                while(m >= b && a[m] > t){
                    a[m+1] = a[m];
                    m--;}
                a[m+1] = t;}
            return;
        }
        if (e - b > 1) {
            // split a[b:e]
            m = b + (e - b) / 2;
            w = b + e - m;
            // wsort -> a[w:e] = sorted    a[b:m]
            //          a[b:m] = reordered a[w:e]
            wsort(a, b, m, w);
            while (w - b > 2) {
                // split a[b:w], w = new mid point
                n = w;
                w = b + (n - b + 1) / 2;
                x = b + n - w;
                // wsort -> a[b:x] = sorted    a[w:n]
                //          a[w:n] = reordered a[b:x]
                wsort(a, w, n, b);
                // wmerge -> a[w:e] = merged    a[b:x]+a[n:e]
                //           a[b:x] = reordered a[w:n]
                wmerge(a, b, x, n, e, w);
            }
            // insert a[b:w] into a[b:e] using left shift
            for (n = w; n > b; --n) {
                t = a[n-1];
                for (m = n; m < e && a[m] < t; ++m)
                    a[m-1] = a[m];
                a[m-1] = t;
            }
        }
    }

    public static void main(String[] args) {
        int[] a = new int[16*1024*1024];
        Random r = new Random(0);
        for(int i = 0; i < a.length; i++)
            a[i] = r.nextInt();
        long bgn, end;
        bgn = System.currentTimeMillis();
        imsort(a, 0, a.length);
        end = System.currentTimeMillis();
        for(int i = 1; i < a.length; i++){
            if(a[i-1] > a[i]){
                System.out.println("failed");
                break;
            }
        }
        System.out.println("milliseconds " + (end-bgn));
    }
}

你好!感谢您的澄清。我所学的算法尝试通过不使用辅助存储来隐式解决空间复杂度问题。我也注意到了这一点,这使得该算法与原始归并排序不同。这可能是我无法复制结果的原因。然而,我正在进行的实验“需要”使用移位方法,这可能是我的结果与标准混合排序不同的原因。再次感谢您的帮助! - Benji Tan
@BenjiTan - 问题中的示例代码在堆栈上使用辅助存储器,由于递归而产生log2(n/s)个堆栈帧。自底向上的归并排序可以避免这种情况。 - rcgldr
当我提交报告时,我会在结论中包含这个 :) 是的,由于较小尺寸下递归次数较少,因此减少了log2(n/s)部分!然而,我现在面临的问题是要得出结论,即这个版本的mergesort与带有移位的hybridsort是否会减少运行时间。从理论上讲,hybridsort仍然应该比mergesort快,但是移位也可能需要时间(因为移位只在hybridsort的插入排序部分中完成)... - Benji Tan
1
@BenjiTan - 我更新了我的答案,展示了一个就地归并排序算法的例子,来自于这个回答,修改为混合就地归并排序/插入排序。它是递归的,因此也在堆栈上使用辅助存储。此外,它不是“稳定”的排序。 - rcgldr
你好!感谢你更新了答案!我已经测试了swap,确实更快!然而,我使用shift(而不是swap)的结果没有什么帮助(只有1-3%的改进)。谢谢你提供有用的见解 :) 编辑:由于我的插入排序使用移位而不是交换,所以我的时间非常高,这可能是结果不明显的原因。这就是项目要求的结果...哈哈! - Benji Tan
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接