本地JavaScript排序比实现的归并排序和快速排序慢

23

我已经实现了归并排序和快速排序,并将它们与本机JavaScript排序进行了比较。对于快速排序,我尝试使用此算法:在youtube上查看算法。两种算法都尽可能地使用较少的内存,对于归并排序,为每个递归调用传递一个辅助数组(以避免开销),对于快速排序,则传递起始和结束位置的位置。 我正在使用这些排序来管理NodeJs应用程序中的大量数据。

以下是归并排序,快速排序和本机JavaScript排序,您可以测试其性能

问题是:为什么本机JavaScript执行得更慢?

在我的情况下:

Chrome - 归并排序:测量:1997.920毫秒; 快速排序:测量:1755.740毫秒; 本机:测量:4988.105毫秒
节点:归并排序:测量:2233.413毫秒; 快速排序:测量:1876.055毫秒; 本机:测量:6317.118毫秒

Merge Sort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}
var mergeSort = function(array) {
  function merge(arr, aux, lo, mid, hi) {
    for (var k = lo; k <= hi; k++) {
      aux[k] = arr[k];
    }

    var i = lo;
    var j = mid + 1;
    for (var k = lo; k <= hi; k++) {
      if (i > mid) {
        arr[k] = aux[j++];
      } else if (j > hi) {
        arr[k] = aux[i++];
      } else if (aux[i] < aux[j]) {
        arr[k] = aux[i++];
      } else {
        arr[k] = aux[j++];
      }
    }
  }

  function sort(array, aux, lo, hi) {
    if (hi <= lo) return;
    var mid = Math.floor(lo + (hi - lo) / 2);
    sort(array, aux, lo, mid);
    sort(array, aux, mid + 1, hi);

    merge(array, aux, lo, mid, hi);
  }

  function merge_sort(array) {
    var aux = array.slice(0);
    sort(array, aux, 0, array.length - 1);
    return array;
  }

  return merge_sort(array);
}


console.time('measure');
mergeSort(arr);
console.timeEnd('measure');
console.log(arr[0], arr[1]);

Quicksort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}

function quickSort(arr, leftPos, rightPos, arrLength) {
  let initialLeftPos = leftPos;
  let initialRightPos = rightPos;
  let direction = true;
  let pivot = rightPos;
  while ((leftPos - rightPos) < 0) {
    if (direction) {
      if (arr[pivot] < arr[leftPos]) {
        quickSort.swap(arr, pivot, leftPos);
        pivot = leftPos;
        rightPos--;
        direction = !direction;
      } else
        leftPos++;
    } else {
      if (arr[pivot] <= arr[rightPos]) {
        rightPos--;
      } else {
        quickSort.swap(arr, pivot, rightPos);
        leftPos++;
        pivot = rightPos;
        direction = !direction;
      }
    }
  }
  if (pivot - 1 > initialLeftPos) {
    quickSort(arr, initialLeftPos, pivot - 1, arrLength);
  }
  if (pivot + 1 < initialRightPos) {
    quickSort(arr, pivot + 1, initialRightPos, arrLength);
  }
}
quickSort.swap = (arr, el1, el2) => {
  let swapedElem = arr[el1];
  arr[el1] = arr[el2];
  arr[el2] = swapedElem;
}
arrLength = arr.length;
console.time('measure');
quickSort(arr, 0, arrLength - 1, arrLength);
console.log(arr[0], arr[1]);
console.timeEnd('measure');

Native Javascript Sort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 100000000));
}

console.time('measure');
arr.sort(function compareNumbers(a, b) {
  return a - b;
});
console.timeEnd('measure');

console.log(arr[0], arr[1]);


就你所写的顺序而言,第一个是最慢的,第二个是最快的,考虑到我的电脑和随机性 :) - Lucio
是的,快速排序表现最快...所以在你的电脑上本地JS比归并排序执行得更好? - Relu Mesaros
有趣。我在Firefox和Edge中检查了它们。在Firefox中,三者之间的差异不是很大,尽管本地排序仍然是最慢的。在Edge中,第一个似乎永远无法完成,或者可能是我放弃得太快了。但是最后两个在Edge中完成了。 - jfriend00
3
如果你将比较器作为参数传递到你的自定义实现中,会怎样呢?这两个自定义实现有硬编码的顺序,而原生实现是通用的。 - zerkms
4
非常可能与香草排序是灵活的并且不能利用本机比较运算符有关。如果您使用compareNumbers而不是<,>,或==重写mergeSort和quickSort,我会很感兴趣看看它们的时间表现如何。 - Hamms
显示剩余20条评论
2个回答

18
那么为什么本地排序更慢?查看代码,原因在于

https://github.com/v8/v8/blob/0c76b0ae850027006d5ec0d92449e449d996d3bb/src/js/array.js#L744

问题似乎在于GetThirdIndex()函数。当分区大小大于1000时,它被调用,我认为它的作用是防止快速排序的最坏情况性能,但是开销很大,因为它创建了一些内部数组对并对它们进行排序,这些对的排序可能会导致进一步递归调用GetThirdIndex()。这与分区原始数组和分区内部数组的递归调用相结合。

由于这些示例的测试数据是随机数据,Relu的快速排序不需要像GetThirdIndex()这样的东西。还有一个检查数组中的“空洞”的方法,但我认为这不重要。

GetThirdIndex()的替代方法是使用中位数算法:

http://en.wikipedia.org/wiki/Median_of_medians

归并排序比快速排序更快,因为采用了防止最坏情况的方法,但需要一个与原始数组相同大小或一半大小的辅助数组。
Introsort是快速排序和堆排序的混合体,如果递归层数过深,则切换到堆排序将是一种替代方案:

http://en.wikipedia.org/wiki/Introsort

下面的第二个归并排序示例使用比较函数进行公平比较。它比本地版本快得多。在Chrome的情况下,比较函数对总时间影响不大。在Firefox的情况下,比较函数的影响更大。在Firefox的情况下,本地版本由于内存不足而失败,因此无法进行比较。
这些是原始帖子中“好奇”的自顶向下归并排序的稍快版本,使用相互递归的函数来避免复制,并且有些优化的merge()(每个比较两个条件)。
使用Firefox的结果(时间略有变化)。
native sort - failed for out of memory.
Relu's merge sort - 1.8 seconds
Relu's quick sort - 1.3 seconds
optimized merge sort - 1.4 seconds
optimized merge sort with compare - 1.8 seconds

使用Chrome的结果(时间略有变化)
native sort - 5.3 seconds
Relu's merge sort - 2.1 seconds
Relu's quick sort - 1.8 seconds
optimized merge sort - 1.6 seconds
optimized merge sort with compare - 1.7 seconds

归并排序

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}
var mergeSort = function(array) {
  function merge(arr, aux, lo, mid, hi) {
    var i = lo;
    var j = mid + 1;
    var k = lo;
    while(true){
      if(arr[i] <= arr[j]){
        aux[k++] = arr[i++];
        if(i > mid){
          do
            aux[k++] = arr[j++];
          while(j <= hi);
          break;
        }
      } else {
        aux[k++] = arr[j++];
        if(j > hi){
          do
            aux[k++] = arr[i++];
          while(i <= mid);
          break;
        }
      }
    }
  }

  function sortarrtoaux(arr, aux, lo, hi) {
    if (hi < lo) return;
    if (hi == lo){
        aux[lo] = arr[lo];
        return;
    }
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoarr(arr, aux, lo, mid);
    sortarrtoarr(arr, aux, mid + 1, hi);
    merge(arr, aux, lo, mid, hi);
  }

  function sortarrtoarr(arr, aux, lo, hi) {
    if (hi <= lo) return;
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoaux(arr, aux, lo, mid);
    sortarrtoaux(arr, aux, mid + 1, hi);
    merge(aux, arr, lo, mid, hi);
  }

  function merge_sort(arr) {
    var aux = arr.slice(0);
    sortarrtoarr(arr, aux, 0, arr.length - 1);
    return arr;
  }

  return merge_sort(array);
}

console.time('measure');
mergeSort(arr);
console.timeEnd('measure');
console.log(arr[0], arr[1]);

使用比较函数的归并排序

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}
var mergeSort = function(array, comparefn) {
  function merge(arr, aux, lo, mid, hi, comparefn) {
    var i = lo;
    var j = mid + 1;
    var k = lo;
    while(true){
      var cmp = comparefn(arr[i], arr[j]);
      if(cmp <= 0){
        aux[k++] = arr[i++];
        if(i > mid){
          do
            aux[k++] = arr[j++];
          while(j <= hi);
          break;
        }
      } else {
        aux[k++] = arr[j++];
        if(j > hi){
          do
            aux[k++] = arr[i++];
          while(i <= mid);
          break;
        }
      }
    }
  }

  function sortarrtoaux(arr, aux, lo, hi, comparefn) {
    if (hi < lo) return;
    if (hi == lo){
        aux[lo] = arr[lo];
        return;
    }
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoarr(arr, aux, lo, mid, comparefn);
    sortarrtoarr(arr, aux, mid + 1, hi, comparefn);
    merge(arr, aux, lo, mid, hi, comparefn);
  }

  function sortarrtoarr(arr, aux, lo, hi, comparefn) {
    if (hi <= lo) return;
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoaux(arr, aux, lo, mid, comparefn);
    sortarrtoaux(arr, aux, mid + 1, hi, comparefn);
    merge(aux, arr, lo, mid, hi, comparefn);
  }

  function merge_sort(arr, comparefn) {
    var aux = arr.slice(0);
    sortarrtoarr(arr, aux, 0, arr.length - 1, comparefn);
    return arr;
  }

  return merge_sort(array, comparefn);
}

console.time('measure');
mergeSort(arr, function compareNumbers(a, b) {
  return a - b;
});
console.timeEnd('measure');
// check result
for (let i = 1; i < length; i++) {
    if(arr[i] < arr[i-1]){
        console.log('error');
        break;
    }
}
console.log(arr[0], arr[1]);

侧面说明:原生排序不稳定。

Native Javascript Sort - test for stability

var length = 100000;
var arr = [];
var j;
for (let i = 0; i < length; i++) {
  j = parseInt(Math.random() * 100);
  arr[i] = [j, i];
}

console.time('measure');
arr.sort(function compareNumbers(a, b) {
  return a[0] - b[0];
});
console.timeEnd('measure');

for (let i = 1; i < length; i++) {
    if( (arr[i][0] == arr[i-1][0]) &&
        (arr[i][1] <  arr[i-1][1]) ){
        console.log('not stable');
        console.log(arr[i-1][0], arr[i-1][1]);
        console.log(arr[i  ][0], arr[i  ][1]);
        break;
    }
}

Native Javascript Sort - change compare to make it stable

var length = 100000;
var arr = [];
var j;
for (let i = 0; i < length; i++) {
  j = parseInt(Math.random() * 100);
  arr[i] = [j, i];
}

console.time('measure');
arr.sort(function compareNumbers(a, b) {
  if(a[0] == b[0])
    return a[1] - b[1];
  return a[0] - b[0];
});
console.timeEnd('measure');

for (let i = 1; i < length; i++) {
    if( (arr[i][0] == arr[i-1][0]) &&
        (arr[i][1] <  arr[i-1][1]) ){
        console.log('not stable');
        console.log(arr[i-1][0], arr[i-1][1]);
        console.log(arr[i  ][0], arr[i  ][1]);
        break;
    }
}


我在想,即使有一个可以处理数字的比较函数,arr.sort()是否仍然会将数字转换为对象。为什么不去检查一下呢?https://github.com/v8/v8/blob/master/src/js/array.js#L712 - zerkms
@rcgldr 不,我的意思是 - 你在假设V8的实现方式而不是去检查它。"转换可能会导致间接级别(如C中的指针)增加排序时间" --- 这个陈述没有意义,因为请参见上面的链接。 - zerkms
@zerkms - 我在我的答案中添加了一个带有比较函数的归并排序,它仍然比原生排序快得多。 - rcgldr
3
我会尽力为您翻译:@zzzzBov - 我更新了我的回答,加入了一个可能导致本地排序变慢的原因。 - rcgldr
1
这个漏洞存在多年了,来自谷歌这样一个以让面试者背诵排序算法而闻名的公司。不确定这是更具讽刺意味还是可悲。 - Dirigible
显示剩余2条评论

0

我不知道14怎么会比76和61大 在此输入图片描述 在此输入图片描述 在此输入图片描述


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