优化的归并排序比快速排序更快
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
的函数留在了合并排序中,并仅在快速排序中执行了>
比较运算符中的内联。尽管它的速度确实略有提升(在 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 个元素的组使用插入排序,以便通过次数保持偶数。合并在每次传递中交替方向,无需复制回,并且通过偶数传递,排序的数据最终在原始数组中。以上是关于优化的归并排序比快速排序更快的主要内容,如果未能解决你的问题,请参考以下文章