jiangtao / blog

深入基础,沉淀下来。欢迎watch或star.

Home Page:https://yuque.com/imjt

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

<漫画算法>常见排序算法整理

jiangtao opened this issue · comments


整理一下之前看《漫画算法》上的一些基础排序算法。部分资料引用wiki自己做过回顾,已经掌握好排序的童鞋,可以忽略。

排序算法

排序算法常见要求:

  1. 时间复杂度 (最好和最坏情况,什么时候最好,什么时候最坏)
  2. 空间复杂度 (最好和最坏情况,什么时候最好,什么时候最坏)
  3. 稳定性 (元素相等的情况下, 排序之后位置是否发生变化)

快速排序 o(n*logn)

  1. 使用条件 普通的数组
  2. 交换排序 (不稳定排序)

具体实现

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

额外空间实现

这种空间复杂度高,代码简洁。

function quickSort2(A) {
  if (A.length < 2) return A
  let pivotIndex = A.length >> 1
  let pivotValue = A.splice(pivotIndex, 1)[0]
  let left = [], right = []
  for(let i = 0; i < A.length; i++) {
    (A[i] > pivotValue ? right : left).push(A[i])
  }
  return quickSort2(left).concat(pivotValue, quickSort2(right))
}

原地排序实现

function swap (A, i, j ) {
  let t = A[j]
  A[j] = A[i]
  A[i] = t
} 
/**
 * 双边循环来做, 好理解
 * 双指针 left与小的比较找到最前面的大的, right找大的比较,找到最后面的小的, 然后交换 
 * 小的就在前面, 大的就在后面咯
 * @param {*} A 
 * @param {*} left 
 * @param {*} right 
 */
function partition (A, left, right) {
  let pivotIndex = left
  let pivot = A[left]
  while(left !== right) {
    while (left < right && A[right] > pivot) {
      right--
    }
    while(left < right && A[left] <= pivot) {
      left++
    }
    if (left < right) {
      swap(A, left, right)
    }
  }
  swap(A, pivotIndex, left)
  return left
}
/**
  * 1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
  * 2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
  * 3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
  **/
function quickSort (A, left = 0, right = A.length - 1) {
  
  if (left < right) {
    const pivot = partition (A, left, right)
    quickSort(A, left, pivot - 1)
    quickSort(A, pivot + 1, right)
  }
  return A
}

冒泡排序 o(n*n)

  1. 使用条件 普通的数组
  2. 交换排序 (稳定排序)
i∈[0,N-1)               //循环N-1遍
   j∈[0,N-1-i)           //每遍循环要处理的无序部分
     swap(j,j+1)          //两两排序(升序/降序)

分为有序区和无序区, 想象就跟水泡一下,沉的在下面 (有序区), 轻的在上面 (无序区)..

  • 有序区不用做比较
  • 有序区放在末端,当判定已经拍完序之后就不用再比较了。
function swap(A, i, j) {
  let t = A[i]
  A[i] = A[j]
  A[j] = t
}
function bubbleSort(A) {
  const len = A.length
  for(let i = 0; i < len; i++) {
    // 假定无序区已排序完, 若本次无序区已排序完,则代表排序已经完成,则退出比较,防止多次交换检测
    let sorted = true
    for(let j = 0; j < len - i - 1;j++) {
      if (A[j] > A[j+1]) {
        swap(A, j, j + 1)
        sorted = false
      }
    }
    if (sorted) break
  }
  return A
}

计数排序 o(n+k)

计数排序主要利用了数组索引自带排序特性

  1. 使用条件 值最大最小值确定, 且范围差不大的情况下(MaxValue - minValue + 1 <= 100)

注: 最大值和最小值差太大, 会造成创建的数组过大, 内存过大;
当数组值不是整数 的时候 不适合用这种方法

  1. 稳定排序

代码实现

// 计数排序适合分布比较均匀的排序
function coutSort(A) {
  const max = Math.max.apply(null, A)
  const min = Math.min.apply(null, A)
  const len = max - min + 1
  const L = new Array(len)
  let R = []
  // 统计数字出现的次数,避免造成不必要undefine的空间浪费, 存储 index (value - min)
  for(let i = 0; i < A.length; i++) {
    let v = A[i] - min
    L[v] >= 1 ? L[v]++ : (L[v] = 1)
  }
  // 利用数组的索引自带排序特性
  for(let i = 0; i < len; i++) {
    while(L[i]) {
      R.push(i + min)
      L[i]--
    }
  }
  return R
}

桶排序

样例数据 [1,2,4,100,50]

原理

  1. 桶排序是对计数排序的补充
  2. 桶的数量 N <= Array.length, 分成 N个桶, 分桶是根据 值所在的区间来分, 比如d = 最大值-最小值, value - 最小值 / d 就是桶的范围比 可求出对应的桶的索引值
  3. 在进行每个桶的排序, 可以是桶自己的排序 也可以其他排序

说明

  1. 桶排序弥补了 计数排序的一些缺陷, 数值必须是整数;

代码实现

function bucketSort (A, bucketLen = A.length) {
  const max = Math.max.apply(null, A)
  const min = Math.min.apply(null, A)
  const d = max - min
  const bucketList = Array.apply(null, {length: bucketLen}).map(_ => [])
  for(let i = 0; i < A.length; i++) {
    // 索引结果: idx = len * d / s 
    let idx = parseInt((A[i] - min) * (bucketLen - 1) / d)
    bucketList[idx].push(A[i])
  }
  for(let i = 0; i < bucketLen; i++) {
    bucketList[i].sort((a, b) => a - b)
  }
  return bucketList.reduce((l, p) => (l = l.concat(p)), [])
}

堆排序

后续补充

归并排序

后续补充