TypeScript 实现各种排序算法

208 阅读4分钟

选择排序

// 原理:遍历一遍选一个最小的放最前面
// 花费:N^2/2 次比较、N 次交换

function selectionSort<Item>(a: Item[]): void {
  const exch = (a: Item[], i: number, j: number): void => {
    const temp: Item = a[i]
    a[i] = a[j]
    a[j] = temp
  }
  for (let i = 0; i < a.length; i++) {
    let min = i
    for (let j = i + 1; j < a.length; j++) if (a[j] < a[min]) min = j
    exch(a, i, min)
  }
}

const a = [5, 3, 6, 1, 543, 24, 56, 123, 444, 325]
selectionSort<number>(a)
console.log(a)

插入排序

// 原理:假设 i 前面的数组有序,将 i 插入前面(通过依次交换位置)
// 花费:对随机无重复数组,插排比选排**快一倍**
// 优化:找到 a[i] 对应的位置之后,将较大元素向右移动,而不是交换两个元素位置,
//       这样可以减少一半的访问数组次数

function insertionSort<Item>(a: Item[]): void {
  const exch = (a: Item[], i: number, j: number): void => {
    const temp: Item = a[i]
    a[i] = a[j]
    a[j] = temp
  }

  for (let i = 1; i < a.length; i++) {
    for (let j = i; j > 0 && a[j] < a[j - 1]; j--) {
      exch(a, j, j - 1)
    }
  }
}

const a = [5, 3, 6, 1, 543, 24, 56, 123, 444, 325]
insertionSort<number>(a)
console.log(a)

希尔排序

// 原理:使数组中间隔为 h 的元素之间有序(h 有序,通过插排(只需要在插排的代码中将移动元素的距离 1 改为 h)),
//       逐渐减小 h 至 1,使数组整体有序。
//       因为插排适合数组较短或部分有序的情况,
//       初期 h 较大,排序的数组较短,后期虽然 h 较小,但是数组已经部分有序。
// 花费:取决于 h 的数学性质,无法准确描述。

function shellSort<Item>(a: Item[]): void {
  const exch = (a: Item[], i: number, j: number): void => {
    const temp: Item = a[i]
    a[i] = a[j]
    a[j] = temp
  }
  const N = a.length
  let h = 1
  while (h < N / 3) h = h * 3 + 1
  while (h >= 1) {
    // i < N,i ++ : 先让每个 h-数组的前两个有序,再遍历每个 h-数组的第三个...
    for (let i = h; i < N; i++) {
      for (let j = i; j >= h && a[j] < a[j - h]; j -= h) {
        exch(a, j, j - h)
      }
    }

    h = Math.floor(h / 3)
  }
}

const a = [5, 3, 6, 1, 543, 24, 56, 123, 444, 325]
shellSort<number>(a)
console.log(a)

归并排序

自顶向下

// 原理:分而治之。要将一个数组排序,可以先将他拆分成两个子数组分别排序,然后再将结果归并起来。
//       归并(简单操作):将两个有序数组归并成一个更大的有序数组。
// 花费:1/2 NlogN - NlogN 次比较,最多 6NlogN 次访问数组(二叉树)
// 优化:对小规模子数组使用插排,因为递归会使小规模问题中方法调用太过频繁。
function mergeSort<Item>(a: Item[]): void {
  const exch = (a: Item[], i: number, j: number): void => {
    const temp: Item = a[i]
    a[i] = a[j]
    a[j] = temp
  }
  // 将 a[lo..mid] 和 a[mid+1..hi] 归并
  const merge = (a: Item[], lo: number, mid: number, hi: number) => {
    let b1 = lo
    let b2 = mid + 1
    const aux = []
    // 复制一份
    for (let i = lo; i <= hi; i++) {
      aux[i] = a[i]
    }

    for (let i = lo; i <= hi; i++) {
      // lo..mid 遍历完了
      if (b1 > mid) a[i] = aux[b2++]
      // mid+1..hi 遍历完了
      else if (b2 > hi) a[i] = aux[b1++]
      else if (aux[b1] < aux[b2]) a[i] = aux[b1++]
      else a[i] = aux[b2++]
    }
  }
  
  const sort = (a: Item[], lo: number, hi: number) => {
    if (hi <= lo) return
    const mid = Math.floor((hi + lo) / 2)
    sort(a, lo, mid)
    sort(a, mid + 1, hi)
    merge(a, lo, mid, hi)
  }

  sort(a, 0, a.length - 1)
}

const a = [5, 3, 6, 1, 543, 24, 56, 123, 444, 325]
mergeSort<number>(a)
console.log(a)

自底向上

// 原理:分治。一一归并 => 两两归并 => 四四归并 => 八八归并 => ...
// 花费:1/2 NlogN - NlogN 次比较,最多 6NlogN 次访问数组

function mergeBUSort<Item>(a: Item[]): void {
  const exch = (a: Item[], i: number, j: number): void => {
    const temp: Item = a[i]
    a[i] = a[j]
    a[j] = temp
  }
  // 将 a[lo..mid] 和 a[mid+1..hi] 归并
  const merge = (a: Item[], lo: number, mid: number, hi: number) => {
    let b1 = lo
    let b2 = mid + 1
    const aux = []
    // 复制一份
    for (let i = lo; i <= hi; i++) {
      aux[i] = a[i]
    }

    for (let i = lo; i <= hi; i++) {
      // lo..mid 遍历完了
      if (b1 > mid) a[i] = aux[b2++]
      // mid+1..hi 遍历完了
      else if (b2 > hi) a[i] = aux[b1++]
      else if (aux[b1] < aux[b2]) a[i] = aux[b1++]
      else a[i] = aux[b2++]
    }
  }
  const N = a.length
  // size 从 1 到 N/2 遍历
  for (let sz = 1; sz < N; sz *= 2) {
    // 遍历数组 归并
    for (let lo = 0; lo < N - sz; lo += sz * 2) {
      merge(a, lo, lo + sz - 1, Math.min(lo + 2 * sz - 1, N - 1))
    }
  }
}

const a = [5, 3, 6, 1, 543, 24, 56, 123, 444, 325]
mergeBUSort<number>(a)
console.log(a)

快速排序

// 原理:分治。先切分后递归。递归后子数组有序,所以整个数组有序。当切分后子数组有序时,整个数组也有序。
// 与归并排序的区别:归并排序先递归后归并。当归并两个有序数组时,结果也有序。
// 花费:平均 2NlnN(即 1.39NlogN)次比较,1/3NlnN 次交换
// 优化:三向切分处理重复元素

function sort<Item>(a: Item[]): void {
  const exch = (a: Item[], i: number, j: number): void => {
    const temp: Item = a[i]
    a[i] = a[j]
    a[j] = temp
  }
  // 获取 lo - hi 间随机索引
  const getRandomIndex = (lo: number, hi: number): number => {
    return lo + Math.floor(Math.random() * (hi - lo + 1))
  }
  // 切分成 a[lo..i-1] a[i] a[i+1..hi]
  const partition = (a: Item[], lo: number, hi: number): number => {
    exch(a, lo, getRandomIndex(lo, hi)) // 将 a[lo] 设为其中随机项,去除输入干扰
    const v = a[lo]
    let pl = lo
    let ph = hi + 1

    while (true) {
      while (a[++pl] < v) {
        if (pl === hi) break
      }
      while (a[--ph] > v) {}
      if (pl >= ph) break
      exch(a, pl, ph)
    }
    // 此时 a[ph] < v,a[pl] > v
    exch(a, lo, ph)
    return ph
  }

  const sort = (a: Item[], lo: number, hi: number): void => {
    if (lo >= hi) return
    const j = partition(a, lo, hi)
    sort(a, lo, j - 1)
    sort(a, j + 1, hi)
  }
  sort(a, 0, a.length - 1)
}

const a = [5, 3, 6, 1, 543, 24, 56, 123, 444, 325]
sort<number>(a)
console.log(a)