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

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

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

快速排序是:

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

这个半缓冲归并排序:

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

我的问题是,为什么在这种情况下半缓冲区合并排序与快速排序的速度相匹配? 另外,我对 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 的链接已失效,所以我无法判断这里比较的是什么。我们可以把相关代码放在这个问题中吗?

标签: javascript node.js algorithm sorting performance-testing


【解决方案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 个元素的组使用插入排序,以便通过次数保持偶数。合并在每次传递中交替方向,无需复制回,并且通过偶数传递,排序的数据最终在原始数组中。
猜你喜欢
  • 1970-01-01
  • 2020-08-14
  • 2020-04-22
  • 1970-01-01
  • 2016-12-23
  • 2021-06-06
  • 2010-10-04
  • 2021-11-28
  • 2012-09-09
相关资源
最近更新 更多