👾 笔记 | Sort 的实现逻辑与排序算法
一、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 中的底层实现
-
分析:
- 当 n <= 10, 采用插入排序
- 当 n > 10, 采用三路快速排序
- 当 10 < n <= 1000, 采用中位数作为哨兵元素
- 当 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)
- 不稳定排序
- 排序过程:
- 排序前先建堆
- 这个堆其实就是一个完全二叉树。如果双亲结点的序号是 i,那么,左孩子节点的序号就是 2 * i + 1,右孩子节点的序号就是 2 * i + 2。
- 在将一个数组调整成一个堆的时候,关键之一的是确定最后一个非叶子节点的序号
- 这个序号为 n / 2-1,n 为序列的长度。
- 可以分两种情形考虑:
- 堆的最后一个非叶子节点若只有左孩子
- 堆的最后一个非叶子节点有左右两个孩子
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 ]
❧