十大排序算法

198 阅读4分钟

4abde1748817d7f35f2bf8b6a058aa40_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.awebp

排序算法的稳定性

在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的

若具有相同关键字的记录之间的相对次序发生变化,则称这种排序方法是不稳定的。

稳定性.png

交换排序类

冒泡排序

比较相邻元素,反序则交换

冒泡排序.gif

function bubbleSort(arr) {
  const len = arr.length - 1
  for (let i = 0; i < len; i++) {
    // 将最大值排到最后
    for (let j = 1; j < len - i - 1; j++) {
      // swap
      if (arr[j] > arr[j + 1]) {
        ;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
      }
    }
  }
}

快速排序

是对冒泡排序算法的一种改进
最坏的情况,待排序的序列是正序 / 反序,时间复杂度:O(n^2)

  1. 首先设定一个基准,将小于基准的值放到数组的左边,大于基准的值放到数组的右边。这个称为分区操作
  2. 递归左右序列进行快速排序,最终排序完成
  • 分而治之
  • 自顶向下拆分排序

快速排序.gif

理解快速排序的思想

  • 时间复杂度 O(n logn)
  • 空间复杂度 O(n logn)
function quickSort(arr) {
  if (arr.length < 2) return arr

  const pivot = arr[0] // 基准元素
  const left = []
  const right = []
  // 左右分开
  for (let i = 1; i < arr.length; i++) {
    const item = arr[i]
    if (item <= pivot) {
      left.push(item)
    } else {
      right.push(item)
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)]
}

原地排序 + 双指针

  • 时间复杂度 O(n logn)
  • 空间复杂度 O(logn)
function quickSort(arr, low = 0, high = arr.length - 1) {
  if (low < high) {
    // 划分 [low..hight] 算出枢轴
    const pivot = partition(arr, low, high)
    // 对低子序列递归排序
    quickSort(arr, low, pivot - 1)
    // 对高子序列递归排序
    quickSort(arr, pivot + 1, high)
  }
}

function partition(arr, low, high) {
  const pivotKey = arr[low] // 基准
  let i = low + 1 // 左指针
  let j = high // 右指针

  while (i <= j) {
    // 移动左指针 - 找到一个比基准大的值
    while (i <= j && arr[i] < pivotKey) {
      i++
    }
    // 移动右指针 - 找到一个比基准小的值
    while (i <= j && arr[j] > pivotKey) {
      j--
    }
    // swap
    if (i <= j) {
      ;[arr[i], arr[j]] = [arr[j], arr[i]]
      // 移动指针
      i++
      j--
    }
  }

  // 最后一个 <= pivotKey 的位置
  // 将基准放到正确位置
  ;[arr[low], arr[j]] = [arr[j], arr[low]]
  return j
}

1. 优化选取基准

如果我们选取的基准是处干整个序列的中间位置,那么我们可以将整个小数集合和大数集合了

排序速度的快慢取决干的基准处在整个序列中的位置,太小或者太大,都会影响性能

1.1 随机选取

const index = Math.floor(Math.random() * (high - low + 1) + low)
;[arr[index], arr[low]] = [arr[low], arr[index]]

const pivot = arr[low]

1.2 三数取中

取三个关键字先进行排序,将中间数作为枢轴

三数取中.png

const mid = (low + high) >> 1
// 交换左端 / 右端,保证左端较小
if (arr[low] > arr[high]) {
  ;[arr[low], arr[high]] = [arr[high], arr[low]]
}
// 交换中间 / 右端,保证中间较小
if (arr[mid] > arr[high]) {
  ;[arr[mid], arr[high]] = [arr[high], arr[mid]]
}
// 交换中间 / 左端,保证左端为中间值
if (arr[mid] > arr[low]) {
  ;[arr[mid], arr[low]] = [arr[low], arr[mid]]
}

// arr[low] 为整个序列左、中、右三个关键字的中间值
const pivotKey = arr[low]

1.3 九数取中

2. 优化小数组时的排序方案

数组非常小时使用直接插入排序

原因:快速排序用到了递归操作,在数据量较大时可以忽略

// 7 / 50 都可以,实际应用可适当调整
const MAX_LENGTH_INSERT_SOFT = 7

function QSort(arr, low, high) {
  // 快速排序
  if (high - low > MAX_LENGTH_INSERT_SOFT) {
    let pivot = partition(arr, low, high)
    QSort(arr, low, pivot - 1)
    QSort(arr, pivot + 1, high)
  }
  // 插入排序
  else {
    insertSort(arr)
  }
}

选择排序类

选择排序

从待排序中选出最小的值,放到已排序的末尾

  • 不稳定:待排序中找到最小值之后,和已排序的末尾元素交换

选择排序.gif

function selectSort(arr) {
  const len = arr.length
  for (let i = 0; i < len - 1; i++) {
    // 找到最小值
    let index = i
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[index]) {
        index = j
      }
    }
    // swap
    if (index !== i) {
      ;[arr[index], arr[i]] = [arr[i], arr[index]]
    }
  }
}

堆排序

插入排序类

(直接)插入排序

将待排序元素,从后向前扫描插入到已排序中

优势:

  1. 数据量较少时
  2. 基本有序

插入排序.webp

function insertSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    const target = arr[i]
    let j = i
    // 反序
    while (j > 0 && arr[j - 1] > target) {
      // 大于目标元素后移一位
      arr[j] = arr[j - 1]
      j--
    }
    // 插入目标元素
    arr[j] = target
  }
}

希尔排序

也称为缩小增量排序,是一种改进的插入排序算法

  1. 通过定义一个增量序列,根据增量将数组分割成多个子序列,对每个子序列进行插入排序。
  2. 随着增量的逐渐缩小,子序列中的元素越来越有序,最终当增量缩小到1时,整个数组将变得基本有序,此时只需进行一次插入排序即可完成排序

希尔排序.png

function shellSort(arr) {
  let gap = arr.length >> 1 // 初始化增量

  // 确保增量最终缩小到 1
  while (gap > 0) {
    // 分组
    for (let i = gap; i < arr.length; i++) {
      // 在当前增量的子序列中进行插入排序
      const temp = arr[i]
      let j = i
      while (j >= gap && arr[j - gap] > temp) {
        arr[j] = arr[j - gap]
        j -= gap
      }
      arr[j] = temp
    }
    // 缩小增量
    gap >>= 1
  }
}

归并排序

  • 分而治之
  • 自顶向下二分拆分,自底向上排序合并

二分:先将待排序数组递归地拆分成两个子序列,直到单个元素
排序:然后对每个子序列进行排序
合并:最后将两个有序子序列合并成一个有序序列

image.png

归并排序.gif

function mergeSort(arr) {
  const len = arr.length
  // 单个元素,结束递归
  if (len < 2) return arr
  // 二分
  const mid = len >> 1
  const left = arr.slice(0, mid)
  const right = arr.slice(mid)
  return merge(arguments.callee(left), arguments.callee(right))
}

function merge(left, right) {
  const stack = []
  // 将两个数组进行排序、合并
  while (left.length && right.length) {
    if (left[0] < right[0]) {
      stack.push(left.shift())
    } else {
      stack.push(right.shift())
    }
  }
  return stack.concat(left, right)
}

非比较类排序

计数排序

将序列中的元素作为键、个数作为值存储在额外的数组空间中,通过遍历该数组排序。

  • 作为一种线性时间复杂度的排序,必须是一定范围内的整数
  • 牺牲空间换取时间

计数排序.webp

基础版

function countingSort(array) {
  // 1. 找出最小值,作为偏移值
  const min = Math.min(...array)
  // 2. 计数
  const counts = []
  for (const v of array) {
    // 统计时减去偏移量
    counts[v - min] = (counts[v - min] || 0) + 1
  }

  // 3. 取出排序
  let index = 0
  for (let i = 0; i < counts.length; i++) {
    let count = counts[i]
    while (count > 0) {
      // 排序时加上偏移量
      array[index] = i + min
      count--
      index++
    }
  }
}

进阶版,保证数组顺序

image.png

function countingSort(arr) {
  // 1. 找出最大、最小值
  const min = Math.min(...arr)
  const max = Math.max(...arr)
  // 2. 计数
    // max-min+1 是计数数组的长度
    // 再 +1 是为了 count[0] 永远为 0
  const count = new Array(max - min + 1 + 1).fill(0)
  for (const num of arr) {
    count[num - min + 1]++
  }

  // 3. 累加之和的值 - 前缀和
  for (let i = 1; i < count.length; i++) {
    count[i] += count[i - 1]
  }

  // 4. 取出排序
  const result = []
  for (const num of arr) {
    // count[num - min] 代表着比 num 小的数的数量
    // 即当前 num 排序后的位置
    result[count[num - min]] = num
    count[num - min]++
  }
  return result
}

参考链接

桶排序

将待排序的元素划分为若干个有序的区间,称为桶(Bucket),然后分别对每个桶内的元素进行排序

  • 适用于对有界范围的整数或浮点数进行排序

桶排序.png

function bucketSort(array) {
  const min = Math.min(...array)
  const max = Math.max(...array)
  // +1 确保能够容纳最大值
  const size = Math.floor((max - min) / array.length) + 1 // 桶的大小
  // const count = Math.floor((max - min) / size) + 1 // 桶的数量
  const buckets = []

  // 将元素分配到桶中
  for (const x of array) {
    const index = Math.floor((x - min) / size)
    ;(buckets[index] ??= []).push(x)
  }

  // 对每个桶中的元素进行插入排序,并合并为结果数组
  const result = []
  for (const bucket of buckets) {
    if (bucket) {
      // result.push(...bucket.sort((a, b) => a - b))
      result.push(...insertionSort(bucket))
    }
  }
  return result
}

基数排序

基数排序也是一个分布式排序算法,它根据数字的有效位或基数将整数分布到桶中进行排序。

基数排序.gif

function radixSort(arr) {
  // 找到数组中的最大值,确定需要进行几轮排序
  const maxNum = Math.max(...arr)
  const maxDigits = String(maxNum).length

  // 根据每个位上的数字进行排序
  for (let i = 0; i < maxDigits; i++) {
    // 此处是使用桶排序,当然也可以使用计数排序
    arr = bucketSort(arr, i)
  }
  return arr
}

// 桶排序(按指定位进行排序)
function bucketSort(arr, digit) {
  // 定义 10 个桶
  // 对于十进制数,只会出现 0-9 的数字,所以使用的基数是 10
  const buckets = Array.from({ length: 10 }, () => [])

  // 将元素分配到对应的桶中
  for (let j = 0; j < arr.length; j++) {
    // 54321 / Math.pow(10, 3) 等于 54.321
    const num = Math.floor(arr[j] / Math.pow(10, digit)) % 10
    buckets[num].push(arr[j])
  }

  // 按照桶的顺序依次输出所有元素
  return [].concat(...buckets)
}

基数排序适用于待排序元素为非负整数的情况

  • 将所有的数加一个正数,使得所有的数变为正数进行基数排序; 排序完之后再减点加的正数值输出
const minValue = Math.min(...arr)
if (minValue < 0) {
  arr = arr.map(i => i - minValue)
}

// 排序结束后
if (minValue < 0) {
  arr = arr.map(i => i + minValue)
}