JS 排序算法

1,236 阅读6分钟

测试数据说明

本文中所有排序方法都通过生成随机数的数组来粗略地统计排序花费的时间,有一定误差,但是整体上能够反映每种排序方式的性能(快慢的性能)。

//测试代码,100000以内的随机数,组成10000个或更多数据的数组
let arr = [];
for (let i=0;i<10000;i++) {
  arr.push(Math.floor(Math.random()*100000));
}
console.time('sort');
sort(arr);
console.timeEnd('sort');

1. 冒泡排序

最简单,效率也最低。

相邻比较,大的放后面,小的放前面。

function bubbleSort (arr) {
    if (arr.length <= 1) return arr;
    let len = arr.length;
    for (let i=0;i<len;i++) {
        for (let j=0;j<len-1-i;j++) {
            if (arr[j] > arr[j+1]) {
                let temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    return arr;
}
//sort: 168.854ms  --  1万数据

2. 快速排序

快速排序是一种效率很高的排序方法, 最简单的实现是通过二分法来实现排序。

取一个基准,数组中的每一项和基准比较,小的放入left,大的放入right, 再对左右盒子递归操作。

function quickSort (arr) {
    if (arr.length <= 1) return arr;
    let pivot = arr[0];
    let len = arr.length, 
        left = [], right = [];
    for (let i=1;i<len;i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort(left).concat(pivot, quickSort(right));
}
// sort: 17.424ms  -- 1万数据
// sort: 86.508ms  -- 10万数据
// sort: 14204.686ms  -- 1000万数据

快速排序之填坑

function quickSort(sortArr) {
    // 递归排序基数左右两边的序列
    function recursive(arr, left, right) {
      if(left >= right)  return;
      let index = partition(arr, left, right);
      recursive(arr, left, index - 1);
      recursive(arr, index + 1, right);
      return arr;
    }
    // 将小于基数的数放到基数左边,大于基数的数放到基数右边,并返回基数的位置
    function partition(arr, left, right) {
      // 取第一个数为基数
      let temp = arr[left];
      while(left < right) {
        while(left < right && arr[right] >= temp)  right--;
        arr[left] = arr[right];
        while(left < right && arr[left] < temp)  left++;
        arr[right] = arr[left];
      }
      // 修改基数的位置
      arr[left] = temp;
      return left;
    }
    return recursive(sortArr, 0, sortArr.length-1);
}
// sort: 6.889ms  -- 1万数据
// sort: 19.873ms  -- 10万数据
// sort: 1405.282ms  -- 1000万数据

3. 选择排序

和冒泡排序一样需要嵌套依次数组的循环,非常耗时。

循环数组,找到最小的数值和第一个数值交换位置,再找到第二小的数值和第二个交换位置,以此类推。

function selectSort (arr) {
  let len = arr.length;
  for (let i=0;i<len;i++) {
    let minIndex = i;
    for (let j=i+1;j<len;j++) {
      minIndex = arr[j] < arr[minIndex] ? j : minIndex;
    }
    if (minIndex !== i) {
      let tmp = arr[i]
      arr[i] = arr[minIndex];
      arr[minIndex] = tmp;
    }
  }
  return arr;
}
// sort: 44.538ms  --  1万数据
// sort: 3802.606ms  -- 10万数据

4. 插入排序

插入排序有两种思路,一种是在原数组中插入,一种是创建一个新的数组,在新数组中插入。

创建一个新数组,循环旧数组,依次拿出数据插入到新数组中正确的位置上。

function insertSort (arr) {
    let len = arr.length;
    let newArr = [];
    for (let i=0;i<len;i++) {
        if (i === 0) {
            newArr.push(arr[i]);
        } else {
            let flag = true;
            for (let j=0;j<newArr.length;j++) {
                if (arr[i] < newArr[j]) {
                    flag = false;
                    newArr.splice(j, 0 arr[i]);
                    break;
                }
            }
            if (flag) newArr.push(arr[i]);
        }
    }
    return newArr;
} 
// sort: 33.506ms  --  1万数据
// sort: 4612.065ms  --  10万数据

在原数组上插入。循环数组,把每个数据和他前面相邻的数据进行比较,把它插入到正确的位置上。

function insertionSort(arr) {
  var len = arr.length;
  var preIndex, current;
  for (var i = 1; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while(preIndex >= 0 && arr[preIndex] > current) {
      arr[preIndex+1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex+1] = current;
  }
  return arr;
}
// sort: 37.907ms -- 1万数据
// sort: 3827.682ms -- 10万数据

5. 希尔排序

插入排序最大的问题是它只能相邻的两个数据比较,计算量大,耗时长。希尔排序是插入排序更高效率的实现。

先将整个待排序记录序列分割成若干个子序列,在序列内分别进行直接插入排序,待整个序列基本有序时,再对全体记录进行一次直接插入排序。希尔排序的核心在于间隔序列的设定。

function shellSort(arr) {
  // gap 即为增量
  var len = arr.length,
      gap = 1;
  while(gap < len/3) {  //动态定义间隔序列
    gap =gap*3+1;
  }
  for (; gap > 0; gap = Math.floor(gap / 3)) {
    for (let i = gap; i < len; i++) {
      let j = i - gap;
      let current = arr[i];
      while(j >= 0 && current < arr[j]) {
        arr[j + gap] = arr[j];
        j = j - gap;
      }
      arr[j + gap] = current;
    }
  }
  return arr;
}
// sort: 7.446ms -- 1万数据
// sort: 26.260ms -- 10万数据
// sort: 2963.541ms -- 1000万数据

6. 归并排序

递归将数组分为两个序列,有序合并这两个序列。

function mergeSort(arr) {
  var len = arr.length;
  if(len < 2) {
      return arr;
  }
  var middle = Math.floor(len / 2),
      left = arr.slice(0, middle),
      right = arr.slice(middle);
  return merge(mergeSort(left), mergeSort(right));

  function merge(left, right) {
    var result = [];

    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }

    while (left.length)
        result.push(left.shift());

    while (right.length)
        result.push(right.shift());

    return result;
  }
}
// sort: 12.854ms -- 1万数据
// sort: 756.277ms -- 10万数据

7. 计数排序

计数排序,桶排序,基数排序这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶
  • 计数排序:每个桶只存储单一键值
  • 桶排序:每个桶存储一定范围的数值
function countingSort(arr) {
    let len = arr.length;
    let maxValue = -Infinity;
    for (let i=0; i<len; i++) {
      if (arr[i] > maxValue) maxValue = arr[i];
    }
  
    var bucket = new Array(maxValue+1),
        sortedIndex = 0;
        bucketLen = maxValue + 1;
  
    for (var i = 0; i < len; i++) {
        if (!bucket[arr[i]]) {
            bucket[arr[i]] = 0;
        }
        bucket[arr[i]]++;
    }
    
    for (var j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
    return arr;
}
// sort: 3.678ms -- 1万数据
// sort: 8.707ms -- 10万数据
// sort: 105.917ms -- 1000万数据

8. 桶排序

要注意Math.max 和 Math.min 如果数据太多会溢出,测试不能超过125662个数据。

function bucketSort(arr) {
    // 桶的个数,只要是正数即可
    let num = 1000;
    let max = Math.max(...arr);
    let min = Math.min(...arr);
    // 计算每个桶存放的数值范围,至少为1,
    let range = Math.ceil((max - min) / num) || 1;
    // 创建二维数组,第一维表示第几个桶,第二维表示该桶里存放的数
    let bucket = Array.from(Array(num)).map(() => Array().fill(0));
    arr.forEach(val => {
      // 计算元素应该分布在哪个桶
      let index = parseInt((val - min) / range);
      // 防止index越界,例如当[5,1,1,2,0,0]时index会出现5
      index = index >= num ? num - 1 : index;
      let temp = bucket[index];
      // 插入排序,将元素有序插入到桶中
      let j = temp.length - 1;
      while(j >= 0 && val < temp[j]) {
        temp[j+1] = temp[j];
        j--;
      }
      temp[j+1] = val;
    })
    // 修改回原数组
    let res = [].concat.apply([], bucket);
    return res;
}
// sort: 7.642ms -- 1万数据
// sort: 17.830ms -- 10万数据

9. 基数排序

function radixSort (arr) {
    let len = arr.length;
    if (len === 0) return arr;
    let count = 1, digits = 1, maxIndex = 0;
    let buckets = [];
    for (let i = 0; i<10; i++) {
      buckets.push(Array(0))
    }
    while (count <= digits) {
      for (let i=0; i<len; i++) {
        let ind = parseInt(arr[i] % Math.pow(10, count) / Math.pow(10, count - 1));
        buckets[ind].push(arr[i]);
        if (count === 1) {
          if (arr[i] > arr[maxIndex]) maxIndex = i;
        }
      }
      if (count === 1) digits = arr[maxIndex].toString().length; //取最大数的位数
      //buckets.flat(1);
      let pos = 0;
      for (let i=0; i<buckets.length; i++) {
        while (buckets[i].length) {
          arr[pos++] = buckets[i].shift();
        }
      }
      count++;
    }
    return arr;
}
// sort: 20.707ms -- 1万数据
// sort: 84.049ms -- 10万数据

参考