笔记 | Sort 的实现逻辑与排序算法

发布时间 2023-08-07 09:14:09作者: iNSlog

一、Sort

Sort() 的功能是对数组元素就地进行排序,会改变数组本身(返回对象同数组的引用)。默认排序顺序是,先将元素转换为字符串后进行排序。

有一个可选参数 compareFunction 定义排序顺序的函数。返回值应该是一个数字,其正负性表示两个元素的相对顺序。

array.sort([compareFunction])。
sort(compareFn?: (a: T, b: T) => number): this;

compareFunction 默认情况下

const stringArr = ['abd', 'abc', 'abe']
stringArr.sort()
console.log(stringArr) // [ 'abc', 'abd', 'abe' ]

const numberArr = [1, 20, 33, 4, 1999]
numberArr.sort()
console.log(numberArr) // [ 1, 1999, 20, 33, 4 ]

指明 compareFunction 的情况下

规则是 compareFunction 函数得到的结果:

  • 等于 0。相对位置不变;
  • 小于 0。a 在 b 的前面;
  • 大于 0。b 在 a 的前面;
const numberArr = [1, 20, 33, 4, 1999]
numberArr.sort((a, b) => 0)
console.log(numberArr) // [ 1, 20, 33, 4, 1999 ]
numberArr.sort((a, b) => b - a)
console.log(numberArr) // [ 1999, 33, 20, 4, 1 ]
numberArr.sort((a, b) => a - b)
console.log(numberArr) // [ 1, 4, 20, 33, 1999 ]

sort 方法在 V8 中的底层实现

  • 分析:

    1. 当 n <= 10, 采用插入排序
    2. 当 n > 10, 采用三路快速排序
    3. 当 10 < n <= 1000, 采用中位数作为哨兵元素
    4. 当 n > 1000, 每隔 200~215 个元素挑出一个元素放到一个新数组中然后对它排序,找到中间位置的数,作为中位数
  • 为什么元素少的时候采用插入排序?

    因为当 n 足够小的时候,快排 nlogn 的优势会越来越小。插入排序经过优化以后,对于小数据集的排序会有非常优越的性能。

  • 为什么要花大力气选择哨兵元素?

    快排的性能瓶颈在于递归深度,最坏情况是每次的哨兵都是极值,在这种情况下,就会有一边是空的。避免这种情况,就要让哨兵尽可能的处于数组的中间位置,让极值情况尽可能的少出现。

二、常用的排序算法

1. 冒泡排序

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(1)
  • 稳定排序

const array = [1, 2, 5, 67, 4, 1, 34, 77, 200, 6, 600, 180, 800]

function bubbleSort(arr) {
    if (arr.length < 2) return arr
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < i; j++) {
            if (arr[j] > arr[i]) {
                const temp = arr[j]
                arr[j] = arr[i]
                arr[i] = temp
            }
        }
    }
    return arr
}
console.log(bubbleSort(array)) 
// [ 1, 1, 2, 4, 5, 6, 34, 67, 77, 180, 200, 600, 800 ]

2. 选择排序

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(1)
  • 不稳定排序
  • 将最小的元素放在序列的起始位置,然后再剩余序列中找到最小的,放到已排序的后面
const array = [1, 2, 5, 67, 4, 1, 34, 77, 200, 6, 600, 180, 800]

function selectSort(arr) {
    if (arr.length < 2) return arr
    const len = arr.length
    let temp
    let minIndex
    for (let i = 0; i < len; i++) {
        minIndex = i
        for (let j = i + 1; j < len; j++) {
            if (arr[j] <= arr[minIndex]) {
                minIndex = j
            }
        }
        temp = arr[i]
        arr[i] = arr[minIndex]
        arr[minIndex] = temp
    }
    return arr
}
console.log(selectSort(array))
// [ 1, 1, 2, 4, 5, 6, 34, 67, 77, 180, 200, 600, 800 ]

3. 快速排序

  • 时间复杂度: O(nlogn)
  • 空间复杂度: O(nlogn)
  • 不稳定排序
  • 快排就是在一趟排序中,将待排序列分隔成两部分,第一部分均比第二部分数值小
const array = [1, 2, 5, 67, 4, 1, 34, 77, 200, 6, 600, 180, 800]

function quickSort(arr) {
    const quick = function (a) {
        if (a.length < 2) return a
        const len = a.length
        const index = Math.floor(len >> 1) // 找到序列中间元素->长度除以二向下取整
        const pivot = a.splice(index, 1)[0] // 在原序列中删除中间元素,当作判断基准
        const left = []
        const right = []
        for (let i = 0; i < len - 1; i++) { // 遍历去除中间元素的数组,所以len要减一
            // 然后序列各项与基准比较,大的放在right数组中,反之放在left数组中
            if (a[i] > pivot) {
                right.push(a[i])
            } else if (a[i] <= pivot) {
                left.push(a[i])
            }
        }
        // 然后递归调用 quick 方法,
        // 对left和right数组进行排序,
        // 将left排序的结果加上中间元素与right排序的结果进行合并,然后返回
        return quick(left).concat([pivot], quick(right))
    }
    return quick(arr)
}
console.info(quickSort(array)) 
// [ 1, 1, 2, 4, 5, 6, 34, 67, 77, 180, 200, 600, 800 ]

4. 插入排序

  • 时间复杂度: O(n^2)
  • 空间复杂度: O(1)
  • 稳定排序
  • 对未排序数据,在已排序序列中进行单方向扫描,找到对应位置进行插入。
const array = [1, 2, 5, 67, 4, 1, 34, 77, 200, 6, 600, 180, 800]

function insertSort(arr) {
    if (arr.length < 2) return arr
    const len = arr.length
    let current
    let prev
    for (let i = 0; i < len; i++) {
        current = arr[i]
        prev = i - 1
        while (prev >= 0 && arr[prev] > current) {
            arr[prev + 1] = arr[prev]
            prev--
        }
        arr[prev + 1] = current
    }
    return arr
}
console.info(insertSort(array)) 
// [ 1, 1, 2, 4, 5, 6, 34, 67, 77, 180, 200, 600, 800 ]

5. 堆排序 (大根堆)

  • 时间复杂度: O(nlogn)
  • 空间复杂度: O(1)
  • 不稳定排序
  • 排序过程:
    1. 排序前先建堆
    2. 这个堆其实就是一个完全二叉树。如果双亲结点的序号是 i,那么,左孩子节点的序号就是 2 * i + 1,右孩子节点的序号就是 2 * i + 2。
    • 在将一个数组调整成一个堆的时候,关键之一的是确定最后一个非叶子节点的序号
    • 这个序号为 n / 2-1,n 为序列的长度。
    • 可以分两种情形考虑:
      1. 堆的最后一个非叶子节点若只有左孩子
      2. 堆的最后一个非叶子节点有左右两个孩子
const array = [1, 2, 5, 67, 4, 1, 34, 77, 200, 6, 600, 180, 800]

function heap_sort(arr) {
    if (arr.length < 2) return arr
    const len = arr.length

    function swap(i, j) { // 对应节点做交换
        let temp = arr[i]
        arr[i] = arr[j]
        arr[j] = temp
    }

    function max_heapify(start, end) {
        let parents = start
        let child = parents * 2 + 1 // 获取左孩子节点的序号
        if (child >= end) return

        // 保证右孩子节点的值大于左孩子节点
        if (child + 1 < end && arr[child] < arr[child + 1]) {
            child++
        }
        // 如果右孩子节点大于双亲节点,进行交换,然后继续建堆
        if (arr[parents] <= arr[child]) {
            swap(parents, child)
            max_heapify(child, end)
        }
    }

    // 第一个循环是处理双亲节点的顺序
    for (let i = Math.floor(len >> 1) - 1; i >= 0; i--) {
        max_heapify(i, len)
    }

    // 第二个循环是根据双亲节点和孩子节点对比的大小,进行堆的调整
    for (let i = len - 1; i > 0; i--) {
        swap(0, i)
        max_heapify(0, i)
    }

    return arr
}
console.log(heap_sort(array)) 
// [ 1, 1, 2, 4, 5, 6, 34, 67, 77, 180, 200, 600, 800 ]

6. 归并排序

  • 时间复杂度: O(nlogn)
  • 空间复杂度: O(n)
  • 稳定排序
  • 将已经有序的子序列合并,得到完全有序的序列。若将两个有序序列合并,叫做二路归并
const array = [1, 2, 5, 67, 4, 1, 34, 77, 200, 6, 600, 180, 800]

function mergeSort(arr) {
    const merge = (right, left) => {
        const result = []
        let il = 0
        let ir = 0
        while (il < left.length && ir < right.length) {
            if (left[il] < right[ir]) {
                result.push(left[il++])
            } else {
                result.push(right[ir++])
            }
        }
        while (il < left.length) {
            result.push(left[il++])
        }
        while (ir < right.length) {
            result.push(right[ir++])
        }
        return result
    }
    const mergeSort = a => {
        if (a.length < 2) return a
        const mid = Math.floor(a.length >> 1)
        const left = a.slice(0, mid)
        const right = a.slice(mid, a.length)
        return merge(mergeSort(right), mergeSort(left))
    }
    return mergeSort(arr)
}
console.log(mergeSort(array)) 
// [ 1, 1, 2, 4, 5, 6, 34, 67, 77, 180, 200, 600, 800 ]