估计大家对 JS 数组的sort 方法已经不陌生了,之前也对它的用法做了详细的总结。那,它的内部是如何来实现的呢?如果说我们能够进入它的内部去看一看,
理解背后的设计,会使我们的思维和素养得到不错的提升。

sort 方法在 V8 内部相对与其他方法而言是一个比较高深的算法,对于很多边界情况做了反复的优化,但是这里我们不会直接拿源码来干讲。我们会来根据源码的思路,实现一个
跟引擎性能一样的排序算法,并且一步步拆解其中的奥秘。

V8 引擎的思路分析

首先大概梳理一下源码中排序的思路:

设要排序的元素个数是n:

  • 当 n <= 10 时,采用插入排序
  • 当 n > 10 时,采用三路快速排序
    • 10 < n <= 1000, 采用中位数作为哨兵元素
    • n > 1000, 每隔 200~215 个元素挑出一个元素,放到一个新数组,然后对它排序,找到中间位置的数,以此作为中位数

在动手之前,我觉得我们有必要为什么这么做搞清楚。

第一、为什么元素个数少的时候要采用插入排序?

虽然插入排序理论上说是O(n^2)的算法,快速排序是一个O(nlogn)级别的算法。但是别忘了,这只是理论上的估算,在实际情况中两者的算法复杂度前面都会有一个系数的,
当 n 足够小的时候,快速排序nlogn的优势会越来越小,倘若插入排序O(n^2)前面的系数足够小,那么就会超过快排。而事实上正是如此,插入排序经过优化以后对于小数据集的排序会有非常优越的性能,很多时候甚至会超过快排。

因此,对于很小的数据量,应用插入排序是一个非常不错的选择。

第二、为什么要花这么大的力气选择哨兵元素?

因为快速排序的性能瓶颈在于递归的深度,最坏的情况是每次的哨兵都是最小元素或者最大元素,那么进行partition(一边是小于哨兵的元素,另一边是大于哨兵的元素)时,就会有一边是空的,那么这么排下去,递归的层数就达到了n, 而每一层的复杂度是O(n),因此快排这时候会退化成O(n^2)级别。

这种情况是要尽力避免的!如果来避免?

就是让哨兵元素进可能地处于数组的中间位置,让最大或者最小的情况尽可能少。这时候,你就能理解 V8 里面所做的种种优化了。

接下来,我们来一步步实现的这样的官方排序算法。

插入排序及优化

最初的插入排序可能是这样写的:

  1. const insertSort = (arr, start = 0, end) => {
  2. end = end || arr.length;
  3. for(let i = start; i < end; i++) {
  4. let j;
  5. for(j = i; j > start && arr[j - 1] > arr[j]; j --) {
  6. let temp = arr[j];
  7. arr[j] = arr[j - 1];
  8. arr[j - 1] = temp;
  9. }
  10. }
  11. return;
  12. }

看似可以正确的完成排序,但实际上交换元素会有相当大的性能消耗,我们完全可以用变量覆盖的方式来完成,如图所示:
1.gif
优化后代码如下:

  1. const insertSort = (arr, start = 0, end) => {
  2. end = end || arr.length;
  3. for(let i = start; i < end; i++) {
  4. let e = arr[i];
  5. let j;
  6. for(j = i; j > start && arr[j - 1] > e; j --)
  7. arr[j] = arr[j-1];
  8. arr[j] = e;
  9. }
  10. return;
  11. }

接下来正式进入到 sort 方法。

寻找哨兵元素

sort的骨架大致如下:

  1. Array.prototype.sort = (comparefn) => {
  2. let array = Object(this);
  3. let length = array.length >>> 0;
  4. return InnerArraySort(array, length, comparefn);
  5. }
  6. const InnerArraySort = (array, length, comparefn) => {
  7. // 比较函数未传入
  8. if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
  9. comparefn = function (x, y) {
  10. if (x === y) return 0;
  11. x = x.toString();
  12. y = y.toString();
  13. if (x == y) return 0;
  14. else return x < y ? -1 : 1;
  15. };
  16. }
  17. const insertSort = () => {
  18. //...
  19. }
  20. const getThirdIndex = (a, from, to) => {
  21. // 元素个数大于1000时寻找哨兵元素
  22. }
  23. const quickSort = (a, from, to) => {
  24. //哨兵位置
  25. let thirdIndex = 0;
  26. while(true) {
  27. if(to - from <= 10) {
  28. insertSort(a, from, to);
  29. return;
  30. }
  31. if(to - from > 1000) {
  32. thirdIndex = getThirdIndex(a, from , to);
  33. }else {
  34. // 小于1000 直接取中点
  35. thirdIndex = from + ((to - from) >> 2);
  36. }
  37. }
  38. //下面开始快排
  39. }
  40. }

我们先来把求取哨兵位置的代码实现一下:

  1. const getThirdIndex = (a, from, to) => {
  2. let tmpArr = [];
  3. // 递增量,200~215 之间,因为任何正数和15做与操作,不会超过15,当然是大于0的
  4. let increment = 200 + ((to - from) & 15);
  5. let j = 0;
  6. from += 1;
  7. to -= 1;
  8. for (let i = from; i < to; i += increment) {
  9. tmpArr[j] = [i, a[i]];
  10. j++;
  11. }
  12. // 把临时数组排序,取中间的值,确保哨兵的值接近平均位置
  13. tmpArr.sort(function(a, b) {
  14. return comparefn(a[1], b[1]);
  15. });
  16. let thirdIndex = tmpArr[tmpArr.length >> 1][0];
  17. return thirdIndex;
  18. }

完成快排

接下来我们来完成快排的具体代码:

  1. const _sort = (a, b, c) => {
  2. let arr = [a, b, c];
  3. insertSort(arr, 0, 3);
  4. return arr;
  5. }
  6. const quickSort = (a, from, to) => {
  7. //...
  8. // 上面我们拿到了thirdIndex
  9. // 现在我们拥有三个元素,from, thirdIndex, to
  10. // 为了再次确保 thirdIndex 不是最值,把这三个值排序
  11. [a[from], a[thirdIndex], a[to - 1]] = _sort(a[from], a[thirdIndex], a[to - 1]);
  12. // 现在正式把 thirdIndex 作为哨兵
  13. let pivot = a[thirdIndex];
  14. [a[from], a[thirdIndex]] = [a[thirdIndex], a[from]];
  15. // 正式进入快排
  16. let lowEnd = from + 1;
  17. let highStart = to - 1;
  18. a[thirdIndex] = a[lowEnd];
  19. a[lowEnd] = pivot;
  20. // [lowEnd, i)的元素是和pivot相等的
  21. // [i, highStart) 的元素是需要处理的
  22. for(let i = lowEnd + 1; i < highStart; i++) {
  23. let element = a[i];
  24. let order = comparefn(element, pivot);
  25. if (order < 0) {
  26. a[i] = a[lowEnd];
  27. a[lowEnd] = element;
  28. lowEnd++;
  29. } else if(order > 0) {
  30. do{
  31. highStart--;
  32. if(highStart === i) break;
  33. order = comparefn(a[highStart], pivot);
  34. }while(order > 0)
  35. // 现在 a[highStart] <= pivot
  36. // a[i] > pivot
  37. // 两者交换
  38. a[i] = a[highStart];
  39. a[highStart] = element;
  40. if(order < 0) {
  41. // a[i] 和 a[lowEnd] 交换
  42. element = a[i];
  43. a[i] = a[lowEnd];
  44. a[lowEnd] = element;
  45. lowEnd++;
  46. }
  47. }
  48. }
  49. // 永远切分大区间
  50. if (lowEnd - from > to - highStart) {
  51. // 继续切分lowEnd ~ from 这个区间
  52. to = lowEnd;
  53. // 单独处理小区间
  54. quickSort(a, highStart, to);
  55. } else if(lowEnd - from <= to - highStart) {
  56. from = highStart;
  57. quickSort(a, from, lowEnd);
  58. }
  59. }

测试结果

测试结果如下:

一万条数据:

1.jpg

十万条数据:

2.jpg

一百万条数据:

3.jpg

一千万条数据:

4.jpg

结果仅供大家参考,因为不同的node版本对于部分细节的实现可能不一样,我现在的版本是v10.15。

从结果可以看到,目前版本的 node 对于有序程度较高的数据是处理的不够好的,而我们刚刚实现的排序通过反复确定哨兵的位置就能
有效的规避快排在这一场景下的劣势。

最后给大家完整版的sort代码:

  1. const sort = (arr, comparefn) => {
  2. let array = Object(arr);
  3. let length = array.length >>> 0;
  4. return InnerArraySort(array, length, comparefn);
  5. }
  6. const InnerArraySort = (array, length, comparefn) => {
  7. // 比较函数未传入
  8. if (Object.prototype.toString.call(comparefn) !== "[object Function]") {
  9. comparefn = function (x, y) {
  10. if (x === y) return 0;
  11. x = x.toString();
  12. y = y.toString();
  13. if (x == y) return 0;
  14. else return x < y ? -1 : 1;
  15. };
  16. }
  17. const insertSort = (arr, start = 0, end) => {
  18. end = end || arr.length;
  19. for (let i = start; i < end; i++) {
  20. let e = arr[i];
  21. let j;
  22. for (j = i; j > start && comparefn(arr[j - 1], e) > 0; j--)
  23. arr[j] = arr[j - 1];
  24. arr[j] = e;
  25. }
  26. return;
  27. }
  28. const getThirdIndex = (a, from, to) => {
  29. let tmpArr = [];
  30. // 递增量,200~215 之间,因为任何正数和15做与操作,不会超过15,当然是大于0的
  31. let increment = 200 + ((to - from) & 15);
  32. let j = 0;
  33. from += 1;
  34. to -= 1;
  35. for (let i = from; i < to; i += increment) {
  36. tmpArr[j] = [i, a[i]];
  37. j++;
  38. }
  39. // 把临时数组排序,取中间的值,确保哨兵的值接近平均位置
  40. tmpArr.sort(function (a, b) {
  41. return comparefn(a[1], b[1]);
  42. });
  43. let thirdIndex = tmpArr[tmpArr.length >> 1][0];
  44. return thirdIndex;
  45. };
  46. const _sort = (a, b, c) => {
  47. let arr = [];
  48. arr.push(a, b, c);
  49. insertSort(arr, 0, 3);
  50. return arr;
  51. }
  52. const quickSort = (a, from, to) => {
  53. //哨兵位置
  54. let thirdIndex = 0;
  55. while (true) {
  56. if (to - from <= 10) {
  57. insertSort(a, from, to);
  58. return;
  59. }
  60. if (to - from > 1000) {
  61. thirdIndex = getThirdIndex(a, from, to);
  62. } else {
  63. // 小于1000 直接取中点
  64. thirdIndex = from + ((to - from) >> 2);
  65. }
  66. let tmpArr = _sort(a[from], a[thirdIndex], a[to - 1]);
  67. a[from] = tmpArr[0]; a[thirdIndex] = tmpArr[1]; a[to - 1] = tmpArr[2];
  68. // 现在正式把 thirdIndex 作为哨兵
  69. let pivot = a[thirdIndex];
  70. [a[from], a[thirdIndex]] = [a[thirdIndex], a[from]];
  71. // 正式进入快排
  72. let lowEnd = from + 1;
  73. let highStart = to - 1;
  74. a[thirdIndex] = a[lowEnd];
  75. a[lowEnd] = pivot;
  76. // [lowEnd, i)的元素是和pivot相等的
  77. // [i, highStart) 的元素是需要处理的
  78. for (let i = lowEnd + 1; i < highStart; i++) {
  79. let element = a[i];
  80. let order = comparefn(element, pivot);
  81. if (order < 0) {
  82. a[i] = a[lowEnd];
  83. a[lowEnd] = element;
  84. lowEnd++;
  85. } else if (order > 0) {
  86. do{
  87. highStart--;
  88. if (highStart === i) break;
  89. order = comparefn(a[highStart], pivot);
  90. }while (order > 0) ;
  91. // 现在 a[highStart] <= pivot
  92. // a[i] > pivot
  93. // 两者交换
  94. a[i] = a[highStart];
  95. a[highStart] = element;
  96. if (order < 0) {
  97. // a[i] 和 a[lowEnd] 交换
  98. element = a[i];
  99. a[i] = a[lowEnd];
  100. a[lowEnd] = element;
  101. lowEnd++;
  102. }
  103. }
  104. }
  105. // 永远切分大区间
  106. if (lowEnd - from > to - highStart) {
  107. // 单独处理小区间
  108. quickSort(a, highStart, to);
  109. // 继续切分lowEnd ~ from 这个区间
  110. to = lowEnd;
  111. } else if (lowEnd - from <= to - highStart) {
  112. quickSort(a, from, lowEnd);
  113. from = highStart;
  114. }
  115. }
  116. }
  117. quickSort(array, 0, length);
  118. }

参考链接:

V8 sort源码(点开第997行)

冴羽排序源码专题

三元博客