JavaScript版的常用排序算法

248 阅读4分钟

记录下 JavaScript 版本十个常用的排序算法。

算法名称时间复杂度空间复杂度是否稳定
冒泡排序O(n^2)O(1)稳定
选择排序O(n^2)O(1)不稳定
插入排序O(n) - O(n^2)O(1)稳定
希尔排序O(n log n)O(1)不稳定
快速排序O(n log n)O(log n)不稳定
三向快速排序O(n) - O(n log n)O(log n)不稳定
归并排序O(n log n)O(n)稳定
堆排序O(n log n)O(1)不稳定
计数排序O(n + k)O(k)稳定
基数排序O(n + k)O(n + k)稳定
桶排序O(n * k)O(n * k)稳定

冒泡排序

  • 每一次比较满足比较结果,都会替换位置
function bubbleSort(arr) {
  let l = arr.length
  for(let i = 0; i < l - 1; i++) {
    for(let j = 0; j < l - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
      }
    }
  }
}

选择排序

  • 比冒泡排序好一点,去与后面未排序的值比较,每一次循环都记录最大值/最小值,在循环结束只会替换一次位置
function selectionSort(arr) {
  let l = arr.length
  let minIndex = 0
  for(let i = 0; i < l - 1; i++) {
    minIndex = i
    for(let j = i + 1; j < l; j++) {
      if (arr[minIndex] > arr[j]) {
        minIndex = j
      }
    }
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
  }
}

插入排序

  • 与选择排序相反,去与前面已排序的值进行比较,每一次满足比较条件都会替换位置
function insertSort(arr) {
  let l = arr.length
  for(let i = 1; i < l; i++) {
    for(let j = i; j > 0; j--) {
      if (arr[j - 1] > arr[j]) {
        [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
      }
    }
  }
}

希尔排序

  • 希尔排序是插入排序的增强版插入排序,插入排序的颗粒度固定为1,而希尔排序的颗粒度是从 N 到 1 的过程。
function shellSort(arr) {
  let len = arr.length
  let h = 1
  while(h < len / 3) h = 3 * h + 1 // 1, 4, 13, 40, 121, 364, 1093
  while(h >= 1) {
    // 将数组变为h有序
    for(let i = h; i < len; i++) {
      for(let j = i; j >= h; j -= h) {
        if (arr[j] < arr[j - h]) {
          [arr[j - h], arr[j]] = [arr[j], arr[j - h]]
        }
      }
    }
    h = Math.floor(h / 3)
  }
}

快速排序

  • 选择一个基准值,比较循环区间内的数据,将比基准值小的移到左侧,大的移到右侧

  • 二切分会对重复的值进行再次比较
  • 三切分不会对重复的值进行比较

二切分快速排序

function quickSort(arr, l, r) {
  if (l >= r) return
  let base = arr[l]
  let i = l
  let j = r + 1

  while(true) {
    while(i <= r && arr[++i] < base) {}
    while(l <= j && arr[--j] > base) {}

    if (i >= j) break;
    
    [arr[i], arr[j]] = [arr[j], arr[i]]
  };

  [arr[l], arr[j]] = [arr[j], arr[l]]

  quickSort(arr, l, j - 1)
  quickSort(arr, j + 1, r)
}

三切分快速排序

function sQuickSort(arr, l, r) {
  if (l >= r) return
  let lf = l
  let ri = r
  let base = arr[lf]
  let i = l + 1

  while(i <= ri) {
    if (base > arr[i]) {
      [arr[lf++], arr[i++]] = [arr[i], arr[lf]]
    } else if (base < arr[i]) {
      [arr[i], arr[ri--]] = [arr[ri], arr[i]]
    } else {
      i++
    }
  }
  sQuickSort(arr, l, lf - 1)
  sQuickSort(arr, ri + 1, r)
}

快速排序是一个不稳定的排序方法,排序的好坏结果取决于原数组值的位置,快速排序最好的结果应该是每次切分都正好在最中间。所以可以在排序前打乱一下数组循序再排序:

for (let i = arr.length - 1; i > 0; i--) {
  let random = Math.random() * i | 0;
  [arr[i], arr[random]] = [arr[random], arr[i]]
}

归并排序

  • 核心是对两个有序数组合并成一个新的有序数组,所以可以将一个无序数组分割成最小粒度(每个子数组1个元素),逐步将整个数组排成有序。
function mergeSort(arr) {
  let aux = new Array(arr.length)

  function merge(a, l, m, r) {
    let i = l, j = m + 1

    for (let k = l;k <= r; k++) {
      aux[k] = a[k]
    }

    for (let k = l; k <= r; k++) {
      if (i > m || aux[j] < aux[i]) {
        a[k] = aux[j++]
      } else {
        a[k] = aux[i++]
      }
    }

    return a
  }
  
  // 自顶向下
  function sort_down(a, l, r) {
    if (l >= r) return
    let m = (l + r) >> 1
    sort_down(a, l, m) // 左边排序
    sort_down(a, m + 1, r) // 右边排序
    if (a[m] > a[m + 1]) {
      merge(a, l, m, r) // 合并
    }
  }

  // 自底向上
  function sort_up(a) {
    let n = a.length
    for (let i = 1; i < n; i += i) {
      for (let j = 0; j < n - i; j += i + i) {
        merge(a, j, i + j - 1, Math.min(j + i + i - 1, n - 1))
      }
    }
  }

  // sort_down(arr, 0, arr.length - 1)
  // sort_up(arr, 0, arr.length - 1)
}

堆排序

  • 利用有序堆的特性:堆顶元素必定是整个堆的最大/最小值,依此求出整个队列的有序性
// 下沉
function sink(arr, k, len) {
  while(2 * k + 1 < len) {
    let j = 2 * k + 1
    if (j < len - 1 && arr[j] < arr[j + 1]) j++
    
    if (arr[k] >= arr[j]) break
    
    [arr[k], arr[j]] = [arr[j], arr[k]]
    k = j
  }
}

// 上浮
function swim(arr, len) {
  let p = 0 // 父级节点
  while(len > 0) {
    p = (len - 1) >> 1

    // (len & 1) 为0的情况下是有兄弟节点的,选出最大的与父级节点比较
    if ((len & 1) === 0 && arr[len] < arr[len - 1]) len--

    if (arr[len] <= arr[p]) break

    [arr[len], arr[p]] = [arr[p], arr[len]]
    len = p
  }
}

function heapSort(arr) {
  let len = arr.length
  // 建立一个有序的堆
  for (let i = (len - 1) >> 1; i >= 0; i--) {
    sink(arr, i, len)
  }
  // 每次将堆顶元素与堆尾元素进行替换,再进行堆顶元素的下沉且堆长度减一,以此可以达到整体有序
  while(len--) {
    [arr[0], arr[len]] = [arr[len], arr[0]]
    sink(arr, 0, len)
  }
}

计数排序

  • 计数排序的思想是直接以数组的值作为一个新数组的下标,然后按下标顺序取出一次放入新数组
function countingSort(arr) {
  let maxValue = Math.max(...arr)
  let bucket = new Array(maxValue + 1).fill(0)
  let sortedIndex = 0


  for (var i = 0; i < arr.length; i++) bucket[arr[i]]++;

  for (var j = 0; j < maxValue + 1; j++) {
    while(bucket[j]) {
      arr[sortedIndex++] = j;
      bucket[j]--;
    }
  }
}

基数排序

  • 对数据的相同位级(个位,百位,千位...)进行排序
function radixSort(nums, n) {
  // 最大二进制位32位,分两次排序,2的16进制最大数为65536,即:1 << 16
  let cnt = new Array(65536).fill(0)
  let temp = new Array(nums.length)
  // 低16位排序
  for (let i = 0; i < nums.length; i++) {
      cnt[nums[i] & 0xffff] += 1
  }
  // 每一个下标的前缀和
  for (let i = 1; i < 65536; i++) {
      cnt[i] += cnt[i - 1]
  }
  // 把数字按照记录的下标放到临时数组
  for (let i = nums.length - 1; i >= 0; i--) {
      temp[--cnt[nums[i] & 0xffff]] = nums[i]
  }
  
  // 重置
  cnt.fill(0)
  // 高16位排序
  for (let i = 0; i < temp.length; i++) {
      cnt[(temp[i] & 0xffff0000) >> 16] += 1
  }
  for (let i = 1; i < 65536; i++) {
      cnt[i] += cnt[i - 1]
  }
  for (let i = nums.length - 1; i >= 0; i--) {
      nums[--cnt[(temp[i] & 0xffff0000) >> 16]] = temp[i]
  }
}

桶排序

  • 桶排序思路
    • 先求出输入数据的最大值与最小值
    • 计算桶的大小区间,然后计算需要桶的数量
    • 循环输入数据,计算每个数据对应的桶ID,然后将数据放入对应的桶
    • 循环桶,用插入排序对每个桶进行排序
    • 最后将桶内的数据有序放入原数组中
function bucketSort(arr) {
  if (arr.length === 0) {
    return arr
  }

  let i
  let minValue = arr[0]
  let maxValue = arr[0]
  for (i = 1; i < arr.length; i++) {
    if (arr[i] < minValue) {
      minValue = arr[i]                 // 输入数据的最小值
    } else if (arr[i] > maxValue) {
      maxValue = arr[i]                 // 输入数据的最大值
    }
  }

  //桶的初始化
  let bucketSize = (arr.length / 10 | 0) + 1
  let bucketCount = ((maxValue - minValue) / bucketSize | 0) + 1
  let buckets = new Array(bucketCount).fill().map(() => [])

  //利用映射函数将数据分配到各个桶中
  for (i = 0; i < arr.length; i++) {
    let bucketIndex = (arr[i] - minValue) / bucketSize | 0
    buckets[bucketIndex].push(arr[i])
  }

  let arrIndex = 0
  for (i = 0; i < buckets.length; i++) {
    insertSort(buckets[i])              // 使用插入排序,对每个桶进行排序
    for (let j = 0; j < buckets[i].length; j++) {
      arr[arrIndex++] = buckets[i][j]                 
    }
  }
  return arr
}