JS排序算法

分析一个排序算法,要从 执行效率、内存消耗、稳定性 三方面入手。

1.执行效率
<1>. 最好情况、最坏情况、平均情况时间复杂度

我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。

<2>. 时间复杂度的系数、常数 、低阶

我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。

但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

<3>. 比较次数和交换(或移动)次数
基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。

所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

2.内存消耗
也就是看空间复杂度。

3.稳定性

经典排序算法

1. 冒泡排序
//冒泡排序(未优化)
const arr = [2,5,4,3,1,0,2];
const bubbleSort = arr => {
  const len = arr.length;
  if(len <= 1) return;
  for(let i=0; i<len-1; i++) {
    for(let j=0; j<len-i-1; j++) {
      if(arr[j] > arr[j+1]) {
        const temp = arr[j];
        arr[j] = arr[j+1];
        arr[j+1] = temp;
      }
    }
  }
}
bubbleSort(arr)

缺点:会把所有都遍历一次

改进方案:

// 冒泡排序(改进)
const arr = [1,0,2,3,4,5,6]
const bubbleSort2 = arr => {
  const len = arr.length;
  if(len <= 1) return;
  for(let i=0; i<len-1; i++) {
    let flag = false;// 提前退出冒泡循环的标志位
    for(let j=0; j<len-i-1; j++) {
      if(arr[j] > arr[j+1]) {
        const temp = arr[j];
        arr[j] = arr[j+1];
        arr[j+1] = temp;
        flag = true; // 表示有数据交换
      }
    }
    
    if(!flag) break; // 如果 false 说明所有元素已经到位,没有数据交换,提前退出
  }
}
bubbleSort2(arr)
console.log(arr)
2. 插入排序

插入排序又为分为 直接插入排序 和优化后的 拆半插入排序 与 希尔排序,我们通常说的插入排序是指直接插入排序。

<1>. 直接插入
插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

const insertionSort = arr => {
  const len = arr.length;
  if(len <= 1) return;

  let 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--; // 游标前移一位
    }
    if(preIndex+1 !== i) {//避免同一个元素赋值给自身
      arr[preIndex + 1] = current; //将当前元素插入预留空位
    }
  }
}

const arr = [7,6,5,4,3,0,1,2]
insertionSort(arr)
console.log(arr)

<2>. 拆半插入
插入排序也有一种优化算法,叫做拆半插入。

折半插入排序是直接插入排序的升级版,鉴于插入排序第一部分为已排好序的数组,我们不必按顺序依次寻找插入点,只需比较它们的中间值与待插入元素的大小即可。

const binaryInsertionSort = arr => {
  const len = arr.length;
  if(len <= 1) return;

  let current, i, j, low, high, m;

  for(i=1; i<len; i++) {
    low = 0;
    high = i-1;
    current = arr[i];
    
    while(low <= high) {
      m = (low + high) >> 1; // x >> 1 == Math.floor(x/2)
      if(arr[i] >= arr[m]) { // 值相同时,切换到高半区,保证稳定性
        low = m+1;  // 插入点在高半区
      }else {
        high = m-1; // 插入点在低半区
      }
    }

    for(j=i; j>low; j--) {
      // 插入位置之后的元素全部后移一位
      arr[j] = arr[j-1]
    }
    arr[low] = current; // 插入该元素
  }
  
}
3. 选择排序

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

const selectionSort = arr => {
  const len = arr.length;

  let minIndex, temp;
  for(let i=0; i<len; 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;
    }
  }
}

const arr = [1,2,8,7,6,3,4,5,0,-5]

selectionSort(arr)
console.log(arr)
4. 归并排序

排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

归并排序采用的是分治思想。

分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

const mergeSort = arr => {
  const len = arr.length;
  if(len <= 1) return arr;
  let m = len >> 1, //length >> 1 == Math.floor(len / 2)
      left = arr.slice(0, m),
      right = arr.slice(m);
  return merge( mergeSort(left), mergeSort(right) );
}

const merge = (left, right) => {
  const 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;
}

const arr = [1,2,8,7,6,3,4,5,0,-5];
console.log(mergeSort(arr))
5. 快速排序

快速排序的特点就是快,而且效率高!它是处理大数据最快的排序算法之一

特点:快速,常用。
缺点:需要另外声明两个数组,浪费了内存空间资源。

const quickSort = arr => {
  const len = arr.length;
  if(len <= 1) return arr; 

  const leftArr = [],
        rightArr = [],
        mIndex = len >> 1;
  const mVal = arr.splice(mIndex, 1)[0];
  
  for(let i=0; i<arr.length; i++) {
    if(arr[i] < mVal) {
      leftArr.push(arr[i])
    }else {
      rightArr.push(arr[i])
    }
  }
  return quickSort(leftArr)
          .concat(mVal)
          .concat( quickSort(rightArr) )
}

const arr = [1,2,8,7,6,3,4,5,0,-5];
console.log(quickSort(arr))
6. 希尔排序

过程

举个易于理解的例子:[35, 33, 42, 10, 14, 19, 27, 44],我们采取间隔 4。创建一个位于 4 个位置间隔的所有值的虚拟子列表。下面这些值是 { 35, 14 },{ 33, 19 },{ 42, 27 } 和 { 10, 44 }。

栗子
我们比较每个子列表中的值,并在原始数组中交换它们(如果需要)。完成此步骤后,新数组应如下所示。

栗子
然后,我们采用 2 的间隔,这个间隙产生两个子列表:{ 14, 27, 35, 42 }, { 19, 10, 33, 44 }。

栗子
我们比较并交换原始数组中的值(如果需要)。完成此步骤后,数组变成:[14, 10, 27, 19, 35, 33, 42, 44],图如下所示,10 与 19 的位置互换一下。

最后,我们使用值间隔 1 对数组的其余部分进行排序,ShellSort 使用插入排序对数组进行排序。

const shellSort = arr => {
  let len = arr.length,
      temp,
      gap = 1;
  while(gap < len/3) {
    gap = gap*3 + 1;    
  }
  for(gap; gap > 0; gap = Math.floor(gap/3)) {
    for(let i=gap; i<len; i++) {
      temp = arr[i];
      let j = i-gap;
      for(; j>= 0 && arr[j] > temp; j-=gap) {
        arr[j+gap] = arr[j];
      }
      arr[j+gap] = temp;
    }
  }
  return arr;
}

const arr = [1,2,8,7,6,3,4,5,0,-5];
console.log(shellSort(arr))
7.堆排序

堆其实是一种特殊的树。只要满足这两点,它就是一个堆。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆。

区分堆、大顶堆、小顶堆
其中图 1 和 图 2 是大顶堆,图 3 是小顶堆,图 4 不是堆。除此之外,从图中还可以看出来,对于同一组数据,我们可以构建多种不同形态的堆。

思想

将初始待排序关键字序列 (R1, R2 … Rn) 构建成大顶堆,此堆为初始的无序区;

将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区 (R1, R2, … Rn-1) 和新的有序区 (Rn) ,且满足 R[1, 2 … n-1] <= R[n]。

由于交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区 (R1, R2 … Rn-1) 调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区 (R1, R2 … Rn-2) 和新的有序区 (Rn-1, Rn)。不断重复此过程,直到有序区的元素个数为 n - 1,则整个排序过程完成。

const heapSort = arr => {
  // 初始化大顶堆,从第一个非叶子结点开始
  for(let i = Math.floor(arr.length/2 -1); i>=0; i--) {
    heapify(arr, i, arr.length);
  }
  // 排序,每一次 for 循环找出一个当前最大值,数组长度减一
  for(let i = Math.floor(arr.length-1); i>0; i--) {
    // 根节点与最后一个节点交换
    swap(arr, 0, i);
    // 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
    heapify(arr, 0, i);
  }
  return arr;
}

// 交换两个节点
const swap = (arr, i, j) => {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
/**
 * 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
 * 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
 * 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
 * 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
 * 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
*/ 
const heapify = (arr, i, length) => {
  let temp = arr[i];
  // j < length 的目的是对结点 i 以下的结点全部做顺序调整
  for(let j=2*i+1; j<length; j=2*j+1) {
    temp = arr[i];
    if(j+1 <length && arr[j] < arr[j+1]) {
      j++;
    }
    if(temp < arr[j]) {
      swap(arr, i, j);
      i = j;
    }else {
      break;
    }
  }  
}

const arr = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log( heapSort(arr) );

复杂度对比

名称 平均 最好 最坏 空间 稳定性 排序方式
冒泡排序 O(n2) O(n) O(n2) O(1) Yes In-place
插入排序 O(n2) O(n) O(n2) O(1) Yes In-place
选择排序 O(n2) O(n2) O(n2) O(1) No In-place
归并排序 O(n log n) O(n log n) O(n log n) O(n) Yes Out-place
快速排序 O(n log n) O(n log n) O(n2) O(logn) No In-place
希尔排序 O(n log n) O(n log2 n) O(n log2 n) O(1) No In-place
堆排序 O(n log n) O(n log n) O(n log n) O(1) No In-place

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值