五大经典排序算法详解(JavaScript)

154 阅读2分钟

一、3 个基础排序算法

1、冒泡排序

外层循环确定最后落点:end 从 arr.length - 1 开始减少 到 1

内层循环从头 0 遍历到 end - 1,大的往后交换

每次内层循环确定一个最终位置

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定
function bubbleSort(arr) {
  if (!arr || arr.length < 2) {
    return
  }
  for (let end = arr.length - 1; end > 0; end--) {
    for (let i = 0; i < end; i++) {
      if (arr[i] > arr[i + 1]) {
        exchange(arr, i, i + 1)
      }
    }
  }
}

2、选择排序

外层循环指定最后落点,从 0 到 arr.length - 2

内层循环从外层指针到 arr.length - 1,找出最小元素的下标,跟外层指针落脚点交换

每次内层循环确定一个最终位置

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定
function selectSort(arr) {
  if (!arr || arr.length < 2) {
    return
  }
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i
    for (let j = i + 1; j < arr.length; j++) {
      minIndex = arr[j] < arr[minIndex] ? j : minIndex
    }
    if (minIndex !== i) {
      exchange(arr, i, minIndex)
    }
  }
}

3、插入排序

外层循环下标前是排好序的

内层循环从外层下标开始往前遍历,小则往前交换,否则跳出内层循环

如果数组基本有序,时间复杂度可以达到 O(n)

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定
function insertSort(arr) {
  if (!arr || arr.length < 2) {
    return
  }
  for (let i = 1; i < arr.length; i++) {
    for (let j = i; j > 0; j--) {
      if (arr[j] < arr[j - 1]) {
        exchange(arr, j, j - 1)
      } else {
        // 由于前面已经有序,不需要再比较
        break
      }
    }
  }
}

二、2 个高级排序

1、归并排序

递归的时间复杂度分析:T(n) = aT(n/b) + O(n^d)

  • log(b,a) > d ==> O(nlog(b,a))
  • log(b,a) = d ==> O(n^dlgn)
  • log(b,a) < d ==> O(n^d)

采用递归,类似二叉树的后序遍历过程(递归、分治:将原问题划分为更小规模合并的过程)

将数组从中间划分为两部分,先分别对两部分递归排序,再 merge 合并两个有序数组(需要辅助数组,由于 javascript 数组的特殊性,可以不用辅助数组)

T(n) = 2T(n/2) + O(n):a = b = 2,d = log(b,a) = 1

  • 时间复杂度:O(nlgn)
  • 空间复杂度:O(n)
  • 稳定
function sort(arr) {
  if (!arr || arr.length < 2) {
    return
  }
  // 递归
  mergeSort(arr, 0, arr.length - 1)
}

function mergeSort(arr, left, right) {
  // 如果区间里只有一个数,不需要排序
  if (left === right) {
    return
  }
  let mid = left + Math.floor((right - left) / 2)
  // 递归排左边
  mergeSort(arr, left, mid)
  // 递归排右边
  mergeSort(arr, mid + 1, right)
  // 合并
  merge(arr, left, mid, right)
}

// 两个有序数组的合并,利用额外空间
function merge(arr, left, mid, right) {
  const helper = []
  let i = 0
  let p1 = left
  let p2 = mid + 1
  while (p1 <= mid && p2 <= right) {
    helper[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]
  }
  // 两个必有且只有一个越界,两个 while 只有一个会执行
  while (p1 <= mid) {
    helper[i++] = arr[p1++]
  }
  while (p2 <= right) {
    helper[i++] = arr[p2++]
  }
  for (i = 0; i < helper.length; i++) {
    arr[left + i] = helper[i]
  }
}

2、快速排序

经典快排:一次只确定一个数的最终位置,受数组情况的影响,有可能导致时间复杂度退化成 O(n²),空间复杂度退化成 O(n)

随机快排:利用概率得到时间复杂度 O(nlgn),空间复杂度 O(lgn)

采用递归,类似二叉树的先序遍历过程

  • 先将数组进行 partition(时间复杂度 O(n),空间复杂度 O(1))
  • 再对左右部分递归进行快排(时间复杂度 O(nlgn),空间复杂度 O(lgn))

与归并相比,虽然都是 O(nlgn),但是快排的常数项小,所以更快一点

  • 时间复杂度:O(nlgn)
  • 空间复杂度:O(lgn):每次划分都要记录下划分的中间位置
  • 不稳定
function sort(arr) {
  if (!arr || arr.length < 2) {
    return
  }
  quickSort(arr, 0, arr.length - 1)
}

function quickSort(arr, left, right) {
  if (left < right) {
    // 随机快排:随机选取一个数字,跟 right 处交换
    exchange(arr, left + Math.floor(Math.random() * (right - left + 1)), right)
    const p = partition(arr, left, right)
    quickSort(arr, left, p[0])
    quickSort(arr, p[1], right)
  }
}

function partition(arr, left, right) {
  let less = left - 1
  let more = right + 1
  let cur = left
  while (cur < more) { // cur = more 时停止
    if (arr[cur] < arr[right]) {
      exchange(arr, ++less, cur++)
    } else if (arr[cur] > arr[right]) {
      exchange(arr, --more, cur)
    } else {
      cur++
    }
  }
  return [less, more] // =num 的区域
}