优化的归并排序比快速排序更快

Posted

技术标签:

【中文标题】优化的归并排序比快速排序更快【英文标题】:Optimized merge sort faster than quicksort 【发布时间】:2016-01-17 22:38:06 【问题描述】:

http://jsperf.com/optimized-mergesort-versus-quicksort

为什么这种半缓冲合并排序的工作速度与快速排序一样快?

快速排序是:

    就地虽然它占用log(n) 递归(堆栈空间) 缓存友好

这个半缓冲归并排序:

    使用n/2 缓冲区进行合并。 使用log(n) 递归。 比较少。

我的问题是,为什么在这种情况下半缓冲区合并排序与快速排序的速度相匹配? 另外,我对 quickSort 做错了什么让它变慢了吗?

function partition(a, i, j) 
    var p = i + Math.floor((j - i) / 2);
    var left = i + 1;
    var right = j;
    swap(a, i, p);
    var pivot = a[i];
    while (left <= right) 
        while (builtinLessThan(a[left], pivot)) 
            ++left;
        
        while (builtinLessThan(pivot, a[right])) 
            --right;
        
        if (left <= right) 
            swap(a, left, right);
            ++left;
            --right;
        
    
    swap(a, i, right);
    return right;
;

function quickSort(a, i, j) 
    var p = partition(a, i, j);
    if ((p) - i > j - p) 
        if (i < p - 1) 
            quickSort(a, i, p - 1);
        
        if (p + 1 < j) 
            quickSort(a, p + 1, j);
        
     else 
        if (p + 1 < j) 
            quickSort(a, p + 1, j);
         if (i < p - 1) 
            quickSort(a, i, p - 1);
        
    
;

【问题讨论】:

在我的盒子上它慢了 30%。 好吧,我正在 chrome 和 Node.js 中测试这个。在节点中,它比快速排序快 2 倍。在 FireFox 中,它和 chrome 一样快 5%。 表扬很便宜。优化的快速排序比合并排序少了约 2.5 倍的交换。所以,如果交换操作便宜,归并排序会更快。否则快速排序将获胜。 我已经测试了昂贵的比较(合并排序要快得多)。但我从没想过测试昂贵的(交换/数组访问)关于如何测试这个的任何想法? 指向 jsperf 的链接已失效,所以我无法判断这里比较的是什么。我们可以把相关代码放在这个问题中吗? 【参考方案1】:

合并排序比快速排序执行更少的比较,但移动更多。必须调用函数来进行比较会增加比较的开销,这会使快速排序变慢。示例快速排序中的所有那些 if 语句也会减慢它的速度。如果比较和交换是内联完成的,那么在对伪随机整数数组进行排序时,快速排序应该会更快一些。

如果在具有 16 个寄存器的处理器上运行,例如在 64 位模式下的 PC,那么使用最终在寄存器中的一堆指针的 4 路合并排序与快速排序一样快。 2路归并排序对每个移动的元素平均进行1次比较,而4路归并排序对每个移动的元素平均进行3次比较,但只需要1/2的遍数,因此基本操作的数量是相同的,但是比较对缓存更友好,使 4 路合并排序快 15%,与快速排序大致相同。

我不熟悉 java 脚本,所以我将示例转换为 C++。

使用转换后的 java 脚本合并排序,对 1600 万个伪随机 32 位整数进行排序大约需要 2.4 秒。下面显示的快速排序示例大约需要 1.4 秒,下面显示的自下而上合并示例大约需要 1.6 秒。如前所述,在具有 16 个寄存器的处理器上使用一堆指针(或索引)进行 4 路合并也需要大约 1.4 秒。

C++ 快速排序示例:

void QuickSort(int a[], int lo, int hi) 
    int i = lo, j = hi;
    int pivot = a[(lo + hi) / 2];
    int t;
    while (i <= j)             // partition
        while (a[i] < pivot)
            i++;
        while (a[j] > pivot)
            j--;
        if (i <= j) 
            t = a[i]
            a[i] = a[j];
            a[j] = t;
            i++;
            j--;
        
    
    if (lo < j)                 // recurse
        QuickSort(a, lo, j);
    if (i < hi)
        QuickSort(a, i, hi);

C++ 自底向上归并排序示例:

void BottomUpMergeSort(int a[], int b[], size_t n)

size_t s = 1;                               // run size 
    if(GetPassCount(n) & 1)                // if odd number of passes
        for(s = 1; s < n; s += 2)           // swap in place for 1st pass
            if(a[s] < a[s-1])
                std::swap(a[s], a[s-1]);
        s = 2;
    
    while(s < n)                           // while not done
        size_t ee = 0;                      // reset end index
        while(ee < n)                      // merge pairs of runs
            size_t ll = ee;                 // ll = start of left  run
            size_t rr = ll+s;               // rr = start of right run
            if(rr >= n)                    // if only left run
                rr = n;
                BottomUpCopy(a, b, ll, rr); //   copy left run
                break;                      //   end of pass
            
            ee = rr+s;                      // ee = end of right run
            if(ee > n)
                ee = n;
            BottomUpMerge(a, b, ll, rr, ee);
        
        std::swap(a, b);                    // swap a and b
        s <<= 1;                            // double the run size
    


void BottomUpMerge(int a[], int b[], size_t ll, size_t rr, size_t ee)

    size_t o = ll;                          // b[]       index
    size_t l = ll;                          // a[] left  index
    size_t r = rr;                          // a[] right index
    while(1)                               // merge data
        if(a[l] <= a[r])                   // if a[l] <= a[r]
            b[o++] = a[l++];                //   copy a[l]
            if(l < rr)                      //   if not end of left run
                continue;                   //     continue (back to while)
            while(r < ee)                   //   else copy rest of right run
                b[o++] = a[r++];
            break;                          //     and return
         else                             // else a[l] > a[r]
            b[o++] = a[r++];                //   copy a[r]
            if(r < ee)                      //   if not end of right run
                continue;                   //     continue (back to while)
            while(l < rr)                   //   else copy rest of left run
                b[o++] = a[l++];
            break;                          //     and return
        
    


void BottomUpCopy(int a[], int b[], size_t ll, size_t rr)

    while(ll < rr)                         // copy left run
        b[ll] = a[ll];
        ll++;
    


size_t GetPassCount(size_t n)               // return # passes

    size_t i = 0;
    for(size_t s = 1; s < n; s <<= 1)
        i += 1;
    return(i);

使用指针的 4 路合并排序的 C++ 示例(goto 用于节省代码空间,它是旧代码)。它开始进行 4 路合并,然后当运行结束时,它切换到 3 路合并,然后是 2 路合并,然后是剩余运行的剩余部分的副本。这类似于用于外部排序的算法,但外部排序逻辑更通用,通常处理多达 16 路合并。

int * BottomUpMergeSort(int a[], int b[], size_t n)

int *p0r;       // ptr to      run 0
int *p0e;       // ptr to end  run 0
int *p1r;       // ptr to      run 1
int *p1e;       // ptr to end  run 1
int *p2r;       // ptr to      run 2
int *p2e;       // ptr to end  run 2
int *p3r;       // ptr to      run 3
int *p3e;       // ptr to end  run 3
int *pax;       // ptr to set of runs in a
int *pbx;       // ptr for merged output to b
size_t rsz = 1; // run size
    if(n < 2)
        return a;
    if(n == 2)
        if(a[0] > a[1])std::swap(a[0],a[1]);
        return a;
    
    if(n == 3)
        if(a[0] > a[2])std::swap(a[0],a[2]);
        if(a[0] > a[1])std::swap(a[0],a[1]);
        if(a[1] > a[2])std::swap(a[1],a[2]);
        return a;
    
    while(rsz < n)
        pbx = &b[0];
        pax = &a[0];
        while(pax < &a[n])
            p0e = rsz + (p0r = pax);
            if(p0e >= &a[n])
                p0e = &a[n];
                goto cpy10;
            p1e = rsz + (p1r = p0e);
            if(p1e >= &a[n])
                p1e = &a[n];
                goto mrg201;
            p2e = rsz + (p2r = p1e);
            if(p2e >= &a[n])
                p2e = &a[n];
                goto mrg3012;
            p3e = rsz + (p3r = p2e);
            if(p3e >= &a[n])
                p3e = &a[n];
            // 4 way merge
            while(1)
                if(*p0r <= *p1r)
                    if(*p2r <= *p3r)
                        if(*p0r <= *p2r)
mrg40:                      *pbx++ = *p0r++;    // run 0 smallest
                            if(p0r < p0e)       // if not end run continue
                                continue;
                            goto mrg3123;       // merge 1,2,3
                         else 
mrg42:                      *pbx++ = *p2r++;    // run 2 smallest
                            if(p2r < p2e)       // if not end run continue
                                continue;
                            goto mrg3013;       // merge 0,1,3
                        
                     else 
                        if(*p0r <= *p3r)
                            goto mrg40;         // run 0 smallext
                         else 
mrg43:                      *pbx++ = *p3r++;    // run 3 smallest
                            if(p3r < p3e)       // if not end run continue
                                continue;
                            goto mrg3012;       // merge 0,1,2
                        
                    
                 else 
                    if(*p2r <= *p3r)
                        if(*p1r <= *p2r)
mrg41:                      *pbx++ = *p1r++;    // run 1 smallest
                            if(p1r < p1e)       // if not end run continue
                                continue;
                            goto mrg3023;       // merge 0,2,3
                         else 
                            goto mrg42;         // run 2 smallest
                        
                     else 
                        if(*p1r <= *p3r)
                            goto mrg41;         // run 1 smallest
                         else 
                            goto mrg43;         // run 3 smallest
                        
                    
                
            
            // 3 way merge
mrg3123:    p0r = p1r;
            p0e = p1e;
mrg3023:    p1r = p2r;
            p1e = p2e;
mrg3013:    p2r = p3r;
            p2e = p3e;
mrg3012:    while(1)
                if(*p0r <= *p1r)
                    if(*p0r <= *p2r)
                        *pbx++ = *p0r++;        // run 0 smallest
                        if(p0r < p0e)           // if not end run continue
                            continue;
                        goto mrg212;            // merge 1,2
                     else 
mrg32:                  *pbx++ = *p2r++;        // run 2 smallest
                        if(p2r < p2e)           // if not end run continue
                            continue;
                        goto mrg201;            // merge 0,1
                    
                 else 
                    if(*p1r <= *p2r)
                        *pbx++ = *p1r++;        // run 1 smallest
                        if(p1r < p1e)           // if not end run continue
                            continue;
                        goto mrg202;            // merge 0,2
                     else 
                        goto mrg32;             // run 2 smallest
                    
                
            
            // 2 way merge
mrg212:     p0r = p1r;
            p0e = p1e;
mrg202:     p1r = p2r;
            p1e = p2e;
mrg201:     while(1)
                if(*p0r <= *p1r)
                    *pbx++ = *p0r++;            // run 0 smallest
                    if(p0r < p0e)               // if not end run continue
                        continue;
                    goto cpy11;
                 else 
                    *pbx++ = *p1r++;            // run 1 smallest
                    if(p1r < p1e)               // if not end run continue
                        continue;
                    goto cpy10;
                
            
            // 1 way copy
cpy11:      p0r = p1r;
            p0e = p1e;
cpy10:      while (1) 
                *pbx++ = *p0r++;                // copy element
                if (p0r < p0e)                  // if not end of run continue
                    continue;
                break;
            
            pax += rsz << 2;            // setup for next set of runs
        
        std::swap(a, b);                // swap ptrs
        rsz <<= 2;                      // quadruple run size
    
    return a;                           // return sorted array

【讨论】:

我不同意上面的说法。我将名为compare 的函数留在了合并排序中,并仅在快速排序中执行了&gt; 比较运算符中的内联。尽管它的速度确实略有提升(在 200,000 个元素上提升了 0.01 秒),但它仍然比具有函数比较开销的归并排序慢 2 倍。 另外,那些额外的 if 语句是优化。快速排序优化之一是递归到分区 FIRST 的较小一侧。这就是所有这些 if 语句的作用。 另外,正在使用递归合并排序实现。唯一优化的是merge 函数。 @Royi - 时间似乎很慢。我有一台使用了 8 年的计算机(Intel 3770K,3.5ghz),对 100,000 个 64 位无符号整数进行标准合并排序需要 6.45 毫秒,比该文章中报告的对 20,000 个元素进行排序的时间要短。我不确定在涉及两次内存比较的循环中 2 个条件与 1 个条件,即使被缓存,也会产生很大的不同。使用 5 切换到插入排序似乎很低。 Visual Studio 模板在 32 个元素处切换。其他版本切换为 32 或 64,这取决于导致自下而上归并排序的偶数次通过。 @Royi - 大多数库使用混合自下而上合并排序和插入排序的一些变体。在我的版本中,我确定纯合并排序的通过次数,如果通过次数为奇数,我对 32 个元素的组使用插入排序,以便合并通过次数为偶数。如果通过次数已经是偶数,我对 64 个元素的组使用插入排序,以便通过次数保持偶数。合并在每次传递中交替方向,无需复制回,并且通过偶数传递,排序的数据最终在原始数组中。

以上是关于优化的归并排序比快速排序更快的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 快速排序

为啥插入排序比快速排序和冒泡排序更快?

C++ 快速排序枢轴优化

Python实现希尔排序快速排序归并排序

算法链表的快速排序和归并排序

算法链表的快速排序和归并排序