JavaScript之排序算法

156 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、冒泡排序

原理: 1.比较相邻的元素。如果第一个比第二个大,就交换两个数;2.对每一对相邻元素重复做步骤一,从开始第一对到结尾的最后一对,该步骤结束会产生一个最大的数;3.针对所有的数重复以上的步骤(最后一个数除外);4.持续每次对越来越少的数重复上面步骤,直到没有任何一对数需要比较。

冒泡排序示意图

function bubbleSort(arr) {
  let len = arr.length
  // 外层循环为执行次数
  for (let i = 0; i < len - 1; i++) {
      let swapped = false;  // 添加一个标志,检测是否有元素交换
      // 外层循环为相邻两个数的比较
      for (let j = 0; j < len - i - 1; j++) {
          if (arr[j] > arr[j + 1]) {
              sort(arr, j, j + 1);
              swapped = false;
          }
     }
     // 如果没有元素交换,则提前退出
     if(!swapped){
         break;
     }
 }
 return arr
}

function sort(arr, i, j) {
 let temp = arr[i]
 arr[i] = arr[j]
 arr[j] = temp
}
let arr = [1, 5, -3, 8, 10]
console.log(bubbleSort(arr)) // [-3, 1, 5, 8, 10]

二、选择排序

原理: 1.首先在末拍序列中找到最小(大)值,存方法到排序序列的起始位置;2.再从剩余末排序列中继续寻找最小(大)值,然后放到已排序序列的末尾;3.重复步骤二,直到所有元素均排序完毕。

选择排序示意图

即:将数组中第一个数标记,然后依次和后面的数进行比较,若找到比标记的数大(小)时再将该数进行标记往后找,直到比较完后再进行位置交换

function selectSort(arr){
      let len = arr.length,
          minIndex,temp;
      for(let i = 0;i < len - 1;i++){
          minIndex = i;
          for(let j = i + 1;j < len;j++){
              if(arr[j] < arr[minIndex]){
                  minIndex = j;
              }
         }
         temp = arr[i];
         arr[i] = arr[minIndex];
         arr[minIndex] = temp;
     }
     return arr;
 }
 let arr = [3,8,9,4,3,0,7]   // [0, 3, 3, 4, 7, 8, 9]
 console.log(selectSort(arr))

三、插入排序

原理: 1.将第一待排序列第一个元素看做一个有序序列,把第二个元素到最后一个函数当成是未排序序列;2.从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果待插入的元素与有序序列中某个元素相等,则将待插入元素插入到相等元素的后面)。

插入排序示意图

function insertSort(arr){
  let len = arr.length,
      preIndex,current;
  for(let 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;
}
let arr = [-5,8,10,99,21,3];
console.log(insertSort(arr))    // [-5, 3, 8, 10, 21, 99]

四、希尔排序

原理: 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序

function shellSort(arr){
var gap = arr.length;
while(gap>1){
  gap = parseInt(gap/2);
  for(var j=gap;j<arr.length;j++){
      var temp = arr[j];
      var i = j-gap;
      while(arr[i]>temp && i>=0){
          arr[i+gap] = arr[i];
         i = i-gap;
     }
     arr[i+gap] = temp;
 }
}
return arr;
}
let arr = [4, 2, 9, 1, 6, 8];
console.log(shellSort(arr))     // [1, 2, 4, 6, 8, 9]

例如:

var arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,1];
// 第一次,增量为7,则分割为:
1-->[3, 26, 1]
2-->[44, 27]
3-->[38, 2]
4-->[5, 46]
5-->[47, 4]
6-->[15, 19]
7-->[36, 50]

// 进行排序后结果为:
[1, 27, 2, 5, 4, 15, 36, 3, 44, 38, 46, 47, 19, 50, 26]
第二次,增量为3,则分割为:
1-->[1,5, 36, 38, 19]
2-->[27, 4, 3, 46, 50]
3-->[2, 15, 44, 47, 26]

// 进行排序后结果为:
[1, 3, 2, 5, 4, 15, 19, 27, 26, 36, 46, 44, 38, 50, 47]

//第三次,增量为1,进行排序后为:
[1, 2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 50]

五、归并排序

原理: 1.把长度为n的输入序列分成两个长度为n/2的子序列;2.对这两个子序列分别采用归并排序;3.将两个排序好的子序列合并成一个最终的排序序列。

归并排序示意图

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;
 }
var arr = [2,5,1,6,-4,10]   
// let arr = [2,1]
console.log(mergeSort(arr))   // [-4,1,2,5,6,10]

六、快速排序

原理: 1.从数列中选出一个数以该数为‘基准’;2.重新排序数列,所有元素比基准值小的排在前面,比基准值大的排在后面(和基准值相等的数可放在任何一边)。在这个分区退出后,该基准就处于数列的中间位置,这个称为分区操作;3.递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。

中心思想: 通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

function quickSort(arr){
      const len = arr.length
      if(len <= 1){
          return arr;
      }
      // 用数组长度的
      let middle = Math.floor(len / 2);
      let base = arr[middle];
      let left = [];
      let right = [];
     for(let i = 0;i < len;i++){
         // 当前索引与基准值索引相等时跳过
         if(i === middle){
             continue;
         }
         if(arr[i] < base){
             left.push(arr[i]);
        }
         else{
             right.push(arr[i])
         }
     }
     return quickSort(left).concat(base,quickSort(right))
 }

 const arr = [2, 9, 1, -5, -10, 6, 8, 7]
 console.log(quickSort(arr))     // [-10, -5, 2, 1, 6, 7, 8, 9]

另一种更加容易理解的实现:

function quickSort(arr){
    if(arr.length <= 1){
        return arr;
    }
    const arrLen = arr.length;
    // 使用数组第一个元素作为基准值
    const baseNum = arr[0];
    const leftArr = [];
    const rightArr = [];
    // 从第二个数开始,大于基准值的放右侧数组,小于基准值的放左侧数组
    for(let i = 1;i < arrLen; i++){
        if(arr[i] > baseNum){
            rightArr.push(arr[i])
        }else{
            leftArr.push(arr[i])
        }
    }
    // 左右两个数组的数据递归调用函数,与基准值合并
    return [...quickSort(leftArr), baseNum, ...quickSort(rightArr)]
}

七、堆排序

原理: 1.将初始待排序关键字序列(R1,R2,...Rn)构建成大顶堆,此堆为初始堆无无序区;2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新堆无序区(R1,R2,...,Rn-1)和新堆有序区(Rn),且满足R[1,2,...,n-1]<R[n];3.由于交换后新堆堆顶R[1]可能违反堆的性质,因此需要堆当前无序区(R1,R2,...,Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2,...,Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程,知道有序区的元素个数为n-1,则整个排序过程完成。

中心思想: 利用这种数据结构设计的一种算法,即自己点的键值或索引总是小于(或大于)它的父节点。

var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量

  function buildMaxHeap(arr) { // 建立大顶堆
      len = arr.length;
      for (var i = Math.floor(len / 2); i >= 0; i--) {
          heapify(arr, i);
      }
  }

 function heapify(arr, i) { // 堆调整
     var left = 2 * i + 1,
         right = 2 * i + 2,
         largest = i;

     if (left < len && arr[left] > arr[largest]) {
         largest = left;
     }

     if (right < len && arr[right] > arr[largest]) {
         largest = right;
     }

     if (largest != i) {
         swap(arr, i, largest);
         heapify(arr, largest);
     }
 }

 function swap(arr, i, j) {
     var temp = arr[i];
     arr[i] = arr[j];
     arr[j] = temp;
 }

 function heapSort(arr) {
     buildMaxHeap(arr);

     for (var i = arr.length - 1; i > 0; i--) {
         swap(arr, 0, i);
         len--;
         heapify(arr, 0);
     }
     return arr;
 }
 var arr = [1, 2, 6, 8, 11, -7, 20]
 console.log(heapSort(arr))      // [-7, 1, 2, 6, 8, 11, 20]

八、计数排序

原理: 1.找出待排序对数组中最大和最小对元素;2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);4.反向填充目标数组,将每个元素i放在心数组的第C(i)项,每放一个元素就将C(i)减去i;

中心思想: 使用额外的数组C,其中第i个元素时待排序数组A中值等于i的元素的个数,然后根据数组C来将A中的元素排到正确的位置,它只能对正整数进行排序。

function countingSort(arr) {
      var len = arr.length;
      var min = max = arr[0],
          result = [];
      for (var i = 1; i < len; i++) {
          max = arr[i] > max ? arr[i] : max;
          min = arr[i] < min ? arr[i] : min;
      }
      var newArr = new Array(max + 1);
     newArr.fill(0);
     for (var i = 0; i < len; i++) {
         newArr[arr[i]] = ++newArr[[arr[i]]];
     }
     for (var i = min - 1; i < max + 1; i++) {
         if (newArr[i] != 0) {
            while (newArr[i]) {
                 result.push(i);
                 --newArr[i];
             }
         }
     }
     return result;
 }

 var arr = [2, 4, 6, 1, 7, 33, 50, 8]
 console.log(countingSort(arr))      // [1, 2, 4, 6, 7, 8, 33, 50]

九、桶排序

原理: 1.设置一个定量的数组,当做 “空桶”;2.遍历输入数据,并且把数组一个一个放到对应的桶里去;3.对每个不是空桶进行排序;4.从不是空的桶里把排好序的数据拼接起来。

中心思想: 计数排序的升级版。假设输入数据服从均匀分布,则数据分到有限数量到桶里,没每个桶再分别排序(有可能再使用别的排序或是以递归方法继续使用桶排序进行排序)

function bucketSort(arr, num = 5) {
      var len = arr.length;
      if (len < 2 || singleArray(arr)) {
          return arr;
      }
      var min = max = arr[0],
          result = [];
      var temp = new Array(num);
      for (var i = 0; i < num; i++) {
         temp[i] = [];
     }
     for (var i = 0; i < len; i++) {
         max = arr[i] > max ? arr[i] : max;
         min = arr[i] < min ? arr[i] : min;
     }
     var gap = (max - min + 1) / num;
     for (var i = 0; i < len; i++) {
         var index = parseInt((arr[i] - min) / gap);
         temp[index].push(arr[i]);
     }
     temp = temp.filter(function (item) {
         return item.length;
     });
     var tempLen = temp.length;
     for (var i = 0; i < tempLen; i++) {
         temp[i] = bucketSort(temp[i]);
         result = result.concat(temp[i]);
    }
     return result;
 }

 function singleArray(arr) { // 判断数组各项是否相同
     var len = arr.length;
     for (var i = 1; i < len; i++) {
         if (arr[i] != arr[0]) {
             return false;
         }
     }
     return true;
 }

 var arr = [1, 3, 8, -6, 11, 7, 99]
 console.log(bucketSort(arr, 5))     // [-6, 1, 3, 7, 8, 11, 99]

例:

var arr = [98,89, 89,1,2,3];
(1)设置桶的数量为5,找到最大值98,最小值1,每个桶的范围(98-1+1)/5=19.6
(2)遍历原始数据,以连标结构放到对应的桶中,数组98,索引值为4(=parseInt(98-1)/19.6),数字89索引值为4(=parseInt(89-1)/19.6),数字123的索引都为00–>[1,2,3]
    桶1–>[]
    桶2–>[]
    桶3–>[]
    桶4–>[98,89,89]
(3)过滤掉为空的桶
(4)递归地利用桶排序(或者其他排序方法)对各个桶中的数组进行排序,如果数组长度为1,或者数组中各项相同(至关重要),则直接返回数组
(5)将各桶合并

十、基数排序

原理: 1. 取得数组中的最大值,并取得位数;2. arr为原始数组,从最低位开始取每个位组成radix数组;3.对radix进行计数排序(利用计数排序适用于小范围的特点)

中心思想: 按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。

:计数排序使用数据范围较小(建议小于1000,且每个数值都要大于0)

function radixSort(arr) {
      var len = arr.length;
      var max = arr[0],
          dev = 1,
          mod = 10;
      for (var i = 1; i < len; i++) {
          max = arr[i] > max ? arr[i] : max;
      }
      var radix = max.toString().length;
     for (var j = 1; j <= radix; j++) {
         var result = [];
         var temp = new Array(10);
         for (var i = 0; i < 10; i++) {
             temp[i] = [];
         }
         for (var i = 0; i < len; i++) {
             var pos = parseInt(arr[i] % mod / dev);
             temp[pos].push(arr[i]);
         }
         dev *= 10;
         mod *= 10;
         for (var i = 0; i < 10; i++) {
             result = result.concat(temp[i]);
         }
         arr = result;
     }
     return arr;
 }
 var arr = [3,6,4,22,12,69]
 console.log(radixSort(arr))     // [3, 4, 6, 12, 22, 69]

排序算法复杂度比较

n:数据规模

k:‘桶’的个数

In-place:占用常数内存,不占用额外内存

Out-place:占用额外内存

稳定:假如 a 在 b 前面,而 a == b,排序之后 a 依然在 b 前面

不稳定:假如 a 在 b 前面,而 a == b,排序之后 a 可能就在 b后面