# 排序算法
# 复杂度
名称 | 最优复杂度 | 平均复杂度 | 最坏复杂度 | 空间复杂度 | 稳定 | 备注 |
---|---|---|---|---|---|---|
冒泡排序 | n | n^2 | n^2 | 1 | Yes | |
插入排序 | n | n^2 | n^2 | 1 | Yes | |
选择排序 | n^2 | n^2 | n^2 | 1 | No | |
堆排序 | n log(n) | n log(n) | n log(n) | 1 | No | |
归并排序 | n log(n) | n log(n) | n log(n) | n | Yes | |
快速排序 | n log(n) | n log(n) | n^2 | log(n) | No | 在 in-place 版本下,内存复杂度通常是 O(log(n)) |
希尔排序 | n log(n) | 取决于差距序列 | n (log(n))^2 | 1 | No | |
计数排序 | n + r | n + r | n + r | n + r | Yes | r - 数组里最大的数 |
基数排序 | n * k | n * k | n * k | n + k ? O(n) ? | Yes | k - 最长 key 的升序 |
|
# 稳定
- 冒泡排序(Bubble Sort) — O(n²)
- 插入排序(Insertion Sort)— O(n²)
- 桶排序(Bucket Sort)— O(n); 需要 O(k) 额外空间
- 计数排序 (Counting Sort) — O(n+k); 需要 O(n+k) 额外空间
- 合并排序(Merge Sort)— O(nlogn); 需要 O(n) 额外空间
- 二叉排序树排序 (Binary tree sort) — O(n log n) 期望时间; O(n²)最坏时间; 需要 O(n) 额外空间
- 基数排序(Radix sort)— O(n·k); 需要 O(n) 额外空间
# 不稳定
- 选择排序(Selection Sort)— O(n²)
- 希尔排序(Shell Sort)— O(nlogn)
- 堆排序(Heapsort)— O(nlogn)
- 快速排序(Quicksort)— O(nlogn) 期望时间, O(n²) 最坏情况; 对于大的、乱数串行一般相信是最快的已知排序
# O(n^2)
- 插入排序
- 选择排序
- 希尔排序
- 冒泡排序
# O(nlogn)
- 快排
- 归并排序
- 堆排序
# O(n)
- 桶排序
- 计数排序
- 基数排序
# 详细
# 1. 冒泡排序(稳定)
原地排序、稳定排序。每次两两比较,大的放到后面。
- 最好 O(n)
- 最坏 O(n^2)
- 平均 O(n^2)
function sort(arr = []) {
const len = arr.length;
// 外层,需要遍历的次数
for (let i = 0; i < len - 1; i++) {
// 内层,每次比较
for (let j = i + 1; j < len; j++) {
if (arr[i] > arr[j]) {
// 大的放到后面
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
return arr;
}
# 例题分析
给定数组 [2, 1, 7, 9, 5, 8]
,要求按照从左到右、从小到大的顺序进行排序。
# 解题思路
从左到右依次冒泡,把较大的数往右边挪动即可。
首先指针指向第一个数,比较第一个数和第二个数的大小,由于 2 比 1 大,所以两两交换,
[1, 2, 7, 9, 5, 8]
。接下来指针往前移动一步,比较 2 和 7,由于 2 比 7 小,两者保持不动,
[1, 2, 7, 9, 5, 8]
。到目前为止,7 是最大的那个数。指针继续往前移动,比较 7 和 9,由于 7 比 9 小,两者保持不动,
[1, 2, 7, 9, 5, 8]
。现在,9 变成了最大的那个数。再往后,比较 9 和 5,很明显,9 比 5 大,交换它们的位置,
[1, 2, 7, 5, 9, 8]
。最后,比较 9 和 8,9 比 8 大,交换它们的位置,
[1, 2, 7, 5, 8, 9]
。经过第一轮的两两比较,9 这个最大的数就像冒泡一样冒到了数组的最后面。
接下来进行第二轮的比较,把指针重新指向第一个元素,重复上面的操作,最后,数组变成了:[1, 2, 5, 7, 8, 9]
。
在进行新一轮的比较中,判断一下在上一轮比较的过程中有没有发生两两交换,如果一次交换都没有发生,就证明其实数组已经排好序了。
# 算法分析
空间复杂度
假设数组的元素个数是 n,由于在整个排序的过程中,我们是直接在给定的数组里面进行元素的两两交换,所以空间复杂度是 O(1)。
# 时间复杂度
1. 给定的数组按照顺序已经排好
在这种情况下,我们只需要进行 n−1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的情况。
2. 给定的数组按照逆序排列
在这种情况下,我们需要进行 n(n-1)/2 次比较,时间复杂度是 O(n2)。这是最坏的情况。
3. 给定的数组杂乱无章
在这种情况下,平均时间复杂度是 O(n2)。
由此可见,冒泡排序的时间复杂度是 O(n2)。它是一种稳定的排序算法。(稳定是指如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变。)
# 2. 快速排序(不稳定)
快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作。
原理:分治 + 递归
- 最好 O(nlogn)
- 最坏 O(n^2)
- 平均 O(nlogn)
/**
* 将数组 arr 分为两部分,前一部分整体小于后一部分
*/
function partition(arr, left, right) {
// 交换数组最左元素与数组的中间元素
let midIndex = ((left + right) / 2) >> 0;
swap(arr, left, midIndex);
// 基准元素
const flagItem = arr[left];
let i = left + 1,
j = right;
while (true) {
while (i <= right && arr[i] < flagItem) {
i++;
}
while (j >= left && arr[j] > flagItem) {
j--;
}
if (i > j) {
break;
} else {
swap(arr, i, j);
i++;
j--;
}
}
swap(arr, left, j);
return j;
}
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left >= right) {
return;
}
const mid = partition(arr, left, right);
quickSort(arr, left, mid - 1);
quickSort(arr, mid + 1, right);
}
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
let leftArr = [];
let rightArr = [];
let q = arr[0];
for (let i = 1, l = arr.length; i < l; i++) {
if (arr[i] > q) {
rightArr.push(arr[i]);
} else {
leftArr.push(arr[i]);
}
}
return [].concat(quickSort(leftArr), [q], quickSort(rightArr));
}
# 三向快速排序
类似快速排序,但是分为了左中右三个部分,中间部分为等于当前值的所有值。适用于有大量重复元素的排序。
const arr = [];
const str = 'SORTEXAMPLE';
for (let char of str) {
arr.push(char);
}
function sort(arr, lo, hi) {
if (hi <= lo) return;
let lt = lo,
i = lo + 1,
gt = hi;
let v = arr[lo];
while (i <= gt) {
if (arr[i] < v) swap(arr, lt++, i++);
else if (arr[i] > v) swap(arr, i, gt--);
else i++;
console.log(arr);
}
sort(arr, lo, lt - 1);
sort(arr, gt + 1, hi);
}
function swap(arr, a, b) {
let x = arr[a];
arr[a] = arr[b];
arr[b] = x;
}
sort(arr, 0, arr.length - 1);
/**
* 三向切分的快速排序指的是,将数组分为小于、等于和大于当前切分值的区域。
* 切分点从开头字母开始
* 擅长于有大量重复元素的情况
*/
/*
初始化状态
["S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
O < S swap(arr, lt++, i++)
["O", "S", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
R < S swap(arr, lt++, i++)
["O", "R", "S", "T", "E", "X", "A", "M", "P", "L", "E"]
T > S swap(arr, i, gt--)
["O", "R", "S", "E", "E", "X", "A", "M", "P", "L", "T"]
E < S
["O", "R", "E", "S", "E", "X", "A", "M", "P", "L", "T"]
E < S
["O", "R", "E", "E", "S", "X", "A", "M", "P", "L", "T"]
X > S
["O", "R", "E", "E", "S", "L", "A", "M", "P", "X", "T"]
["O", "R", "E", "E", "L", "S", "A", "M", "P", "X", "T"]
["O", "R", "E", "E", "L", "A", "S", "M", "P", "X", "T"]
["O", "R", "E", "E", "L", "A", "M", "S", "P", "X", "T"]
["O", "R", "E", "E", "L", "A", "M", "P", "S", "X", "T"]
S 切分完毕 ["O", "R", "E", "E", "L", "A", "M", "P",|<-| "S",|->| "X", "T"]
["O", "P", "E", "E", "L", "A", "M", "R", "S", "X", "T"]
["O", "M", "E", "E", "L", "A", "P", "R", "S", "X", "T"]
["M", "O", "E", "E", "L", "A", "P", "R", "S", "X", "T"]
["M", "E", "O", "E", "L", "A", "P", "R", "S", "X", "T"]
["M", "E", "E", "O", "L", "A", "P", "R", "S", "X", "T"]
["M", "E", "E", "L", "O", "A", "P", "R", "S", "X", "T"]
["M", "E", "E", "L", "A", "O", "P", "R", "S", "X", "T"]
["E", "M", "E", "L", "A", "O", "P", "R", "S", "X", "T"]
["E", "E", "M", "L", "A", "O", "P", "R", "S", "X", "T"]
["E", "E", "L", "M", "A", "O", "P", "R", "S", "X", "T"]
["E", "E", "L", "A", "M", "O", "P", "R", "S", "X", "T"]
["E", "E", "L", "A", "M", "O", "P", "R", "S", "X", "T"]
["E", "E", "A", "L", "M", "O", "P", "R", "S", "X", "T"]
["A", "E", "E", "L", "M", "O", "P", "R", "S", "X", "T"]
["A", "E", "E", "L", "M", "O", "P", "R", "S", "X", "T"]
["A", "E", "E", "L", "M", "O", "P", "R", "S", "T", "X"]
*/
# 实现
把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。
举例:把班里的所有同学按照高矮顺序排成一排。
解法:老师先随机地挑选了同学 A,让所有其他同学和 A 比高矮,比 A 矮的都站在 A 的左边,比 A 高的都站在 A 的右边。接下来,老师分别从左边和右边的同学里选择了同学 B 和 C,然后不断地筛选和排列下去。
在分成较小和较大的两个子数组过程中,如何选定一个基准值(也就是同学 A、B、C 等)尤为关键。
# 例题分析
对数组 [2, 1, 7, 9, 5, 8]
进行排序。
# 解题思路
按照快速排序的思想,首先把数组筛选成较小和较大的两个子数组。
随机从数组里选取一个数作为基准值,比如 7,于是原始的数组就被分成了两个子数组。注意:快速排序是直接在原始数组里进行各种交换操作,所以当子数组被分割出来的时候,原始数组里的排列也被改变了。
接下来,在较小的子数组里选 2 作为基准值,在较大的子数组里选 8 作为基准值,继续分割子数组。
继续将元素个数大于 1 的子数组进行划分,当所有子数组里的元素个数都为 1 的时候,原始数组也被排好序了。
# 时间复杂度
1. 最优情况:被选出来的基准值都是当前子数组的中间数。
这样的分割,能保证对于一个规模大小为 n 的问题,能被均匀分解成两个规模大小为 n/2 的子问题(归并排序也采用了相同的划分方法),时间复杂度就是:T(n) = 2×T(n/2) + O(n)。
把规模大小为 n 的问题分解成 n/2 的两个子问题时,和基准值进行了 n-1 次比较,复杂度就是 O(n)。很显然,在最优情况下,快速排序的复杂度也是 O(nlogn)。
2. 最坏情况:基准值选择了子数组里的最大或者最小值
每次都把子数组分成了两个更小的子数组,其中一个的长度为 1,另外一个的长度只比原子数组少 1。
举例:对于数组来说,每次挑选的基准值分别是 9、8、7、5、2。
解法:划分过程和冒泡排序的过程类似。
算法复杂度为 O(n2)。
提示:可以通过随机地选取基准值来避免出现最坏的情况。
# 空间复杂度
和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成交换操作实现直接对数组的修改,又因为递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数,因此它的空间复杂度是 O(logn)。
举例:LeetCode 第 215 题,给定一个尚未排好序的数组,要求找出第 k 大的数。
解法 1:直接将数组进行排序,然后得出结果。
解法 2:快速排序。
每次随机选取一个基准值,将数组分成较小的一半和较大的一半,然后检查这个基准值最后所在的下标是不是 k,算法复杂度只需要 O(n)。
# 3. 选择排序(不稳定)
选择排序的原理如下。遍历数组,设置最小值的索引为 0,如果取出的值比当前最小值小,就替换最小值索引,遍历完成后,将第一个元素和最小值索引上的值交换。如上操作后,第一个元素就是数组中的最小值,下次遍历就可以从索引 1 开始重复上述操作。
原地排序;不稳定排序
选择排序:选中数组中第 i 小的值与第 i 个值进行调换位置。
原理:每次从无序序列选择一个最小的。每一轮从数组的未排序部分加一开始,找到一个最小的值的索引,然后与未排序将其放到未排序部分的最左位置。
复杂度:
- 最好 O(n^2)
- 最坏 O(n^2)
- 平均 O(n^2)
选择排序是每次都在寻找最小值。 已排序部分是从小到大排好的。
选择排序,从头至尾扫描序列,找出最小的一个元素,和第一个元素交换,接着从剩下的元素中继续这种选择和交换方式,最终得到一个有序序列。
function sort(arr) {
const len = arr.length;
for (let i = 0; i < len; i++) {
let min = i;
// 找到第 n 个最小值。 在 arr[i + 1, arr.length - 1] 中找最小值索引, i+1 代表有序的下一个数,我们默认第一个元素是最小的
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[min]) min = j;
}
// 每次循环, a[i] 位都将是未选择出的数据中的最小值
if (min !== i) {
[arr[i], arr[min]] = [arr[min], arr[i]];
}
}
return arr;
}
import Sort from '../Sort';
export default class SelectionSort extends Sort {
sort(originalArray) {
// Clone original array to prevent its modification.
const array = [...originalArray];
for (let i = 0; i < array.length - 1; i += 1) {
let minIndex = i;
// Call visiting callback.
this.callbacks.visitingCallback(array[i]);
// Find minimum element in the rest of array.
for (let j = i + 1; j < array.length; j += 1) {
// Call visiting callback.
this.callbacks.visitingCallback(array[j]);
if (this.comparator.lessThan(array[j], array[minIndex])) {
minIndex = j;
}
}
// If new minimum element has been found then swap it with current i-th element.
if (minIndex !== i) {
[array[i], array[minIndex]] = [array[minIndex], array[i]];
}
}
return array;
}
}
/**
* 选择排序
* @param : <Array> target数组
* @description : 一次内循环得到最大值的下标,然后只交换一次次序,将最大值和内循环末尾对调。
*/
module.exports.selectSort = function selectSort(target) {
for (var j = target.length; j > 0; j--) {
var maxIndex = 0;
for (var i = 1; i < j; i++) {
maxIndex = target[maxIndex] > target[i] ? maxIndex : i;
}
target.swap(maxIndex, j - 1);
}
return target;
};
# 4. 插入排序 (稳定)
插入排序的原理如下。第一个元素默认是已排序元素,取出下一个元素和当前元素比较,如果当前元素大就交换位置。那么此时第一个元素就是当前的最小数,所以下次取出操作从第三个元素开始,向前对比,重复之前的操作。
插入排序:选中某个值,将这个值移动到 a[i - 1] < a[i] < a[i + 1]
位置。
原理:从有序序列中选择合适的位置进行插入
复杂度:
- 最好 O(n)
- 最坏 O(n^2)
- 平均 O(n^2)
function sort(arr) {
const len = arr.length;
for (let i = 1; i < len; i++) {
// 0 - i 已经是有序序列。
for (let j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
swap(arr, j, j - 1);
console.log(arr);
}
}
return arr;
}
function swap(arr, a, b) {
let x = arr[a];
arr[a] = arr[b];
arr[b] = x;
}
为当前元素保存一个副本,依次向前遍历前面的元素是否比自己大,如果比自己大就直接把前一个元素赋值到当前元素的位置,当前某位置的元素不再比当前元素大的时候,将当前元素的值赋值到这个位置。
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
let j,
temp = arr[i];
for (j = i; j > 0 && arr[j - 1] > temp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
// 插入排序
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12],
length = b.length;
function insertSort(a) {
for (let i = 1; i < length - 1; i++) {
// console.log(a[i])
// 内循环执行完之后排序好前面的队列
// i + 1 就是当前需要插入的值.
for (let j = i + 1; j >= 0; j--) {
if (a[j] < a[j - 1]) {
[a[j - 1], a[j]] = [a[j], a[j - 1]];
// console.log('交换: ', a)
} else {
// 如果比当前最大的值还大,就不用继续比较了
break;
}
}
}
return a;
}
console.log(insertSort(b));
优化版
let intensifyInsertArray = copyArray(arr);
function intensifyInsertSort(arr) {
for (let i = 1; i < arr.length; i++) {
let temp = arr[i],
j;
for (j = i; j > 0 && arr[j - 1] > temp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
return arr;
}
插入排序也是一个简单的排序算法,它的思想是,每次只处理一个元素,从后往前查找,找到该元素合适的插入位置,最好的情况下,即正序有序(从小到大),这样只需要比较 n 次,不需要移动。因此时间复杂度为 O(n) ,最坏的情况下,即逆序有序,这样每一个元素就需要比较 n 次,共有 n 个元素,因此实际复杂度为 O(n²) 。
# 算法实现:
export default function InsertionSort(items: Array<number>): Array<number> {
const itemsCopy = [...items];
let value; // the value currently being compared
let i; // index of first element in unsorted section
let j; // index going into sorted section
for (i = 0; i < itemsCopy.length; i++) {
value = itemsCopy[i];
j = i - 1;
while (j >= 0 && itemsCopy[j] > value) {
itemsCopy[j + 1] = itemsCopy[j];
j--;
}
itemsCopy[j + 1] = value;
}
return itemsCopy;
}
/**
* 直接插入排序
* @param : <Array> target数组
* @description : 将当前元素与前面元素逐一比较,比前方元素小时则将前方元素后移,直到比前方元素大则落位
*/
module.exports.straightInsertionSort = function straightInsertionSort(target) {
for (let i = 1; i < target.length; i++) {
var j = i;
var base = target[i];
while (j > 0 && base < target[j - 1]) {
target[j] = target[j - 1];
j--;
}
if (j < i) {
target[j] = base;
}
}
return target;
};
# 插入排序(Insertion Sort)
# 基本思想
不断地将尚未排好序的数插入到已经排好序的部分。
# 特点
在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的;而对于插入排序来说,经过每一轮的排序处理后,数组前端的数都是排好序的。
# 例题分析
对数组 [2, 1, 7, 9, 5, 8]
进行插入排序。
# 解题思路
首先将数组分成左右两个部分,左边是已经排好序的部分,右边是还没有排好序的部分,刚开始,左边已排好序的部分只有第一个元素 2。接下来,我们对右边的元素一个一个进行处理,将它们放到左边。
先来看 1,由于 1 比 2 小,需要将 1 插入到 2 的前面,做法很简单,两两交换位置即可,
[1, 2, 7, 9, 5, 8]
。然后,我们要把 7 插入到左边的部分,由于 7 已经比 2 大了,表明它是目前最大的元素,保持位置不变,
[1, 2, 7, 9, 5, 8]
。同理,9 也不需要做位置变动,
[1, 2, 7, 9, 5, 8]
。接下来,如何把 5 插入到合适的位置。首先比较 5 和 9,由于 5 比 9 小,两两交换,
[1, 2, 7, 5, 9, 8]
,继续,由于 5 比 7 小,两两交换,[1, 2, 5, 7, 9, 8]
,最后,由于 5 比 2 大,此轮结束。最后一个数是 8,由于 8 比 9 小,两两交换,
[1, 2, 5, 7, 8, 9]
,再比较 7 和 8,发现 8 比 7 大,此轮结束。到此,插入排序完毕。
# 代码示例
void sort(int`[]` nums) {
// 将数组的第一个元素当作已经排好序的,从第二个元素,即 i 从 1 开始遍历数组
for (int i = 1, j, current; i < nums.length; i++) {
// 外围循环开始,把当前 i 指向的值用 current 保存
current = nums`[i]`;
// 指针 j 内循环,和 current 值比较,若 j 所指向的值比 current 值大,则该数右移一位
for (j = i \- 1; j >= 0 && nums`[j]` > current; j\-\-) {
nums`[j + 1]` = nums`[j]`;
}
// 内循环结束,j+1 所指向的位置就是 current 值插入的位置
nums`[j + 1]` = current;
}
}
# 算法分析
# 空间复杂度
假设数组的元素个数是 n,由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,空间复杂度是 O(1)。
# 时间复杂度
1. 给定的数组按照顺序已经排好
只需要进行 n-1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的情况。
2. 给定的数组按照逆序排列
在这种情况下,我们需要进行 n(n-1)/2 次比较,时间复杂度是 O(n2)。这是最坏的情况。
3. 给定的数组杂乱无章
在这种情况下,平均时间复杂度是 O(n2)。
由此可见,和冒泡排序一样,插入排序的时间复杂度是 O(n2),并且它也是一种稳定的排序算法。
建议:LeetCode 第 147 题,要求对一个链表进行插入排序,希望大家去试一试。
# 归并排序(Merge Sort)
# 基本思想
核心是分治,就是把一个复杂的问题分成两个或多个相同或相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,最原问题的解就是子问题解的合并。归并排序将分治的思想体现得淋漓尽致。
# 实现
一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素,才开始排序。
排序的方法就是按照大小顺序合并两个元素,接着依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。
# 例题分析
例题:利用归并排序算法对数组 [2, 1, 7, 9, 5, 8]
进行排序。
# 解题思路
首先不断地对数组进行切分,直到各个子数组里只包含一个元素。
接下来递归地按照大小顺序合并切分开的子数组,递归的顺序和二叉树里的前向遍历类似。
合并
[2]
和[1]
为[1, 2]
。子数组
[1, 2]
和[7]
合并。右边,合并
[9]
和[5]
。然后合并
[5, 9]
和[8]
。最后合并
[1, 2, 7]
和[5, 8, 9]
成[1, 2, 5, 8, 9]
,就可以把整个数组排好序了。
合并数组 [1, 2, 7]
和 [5, 8, 9]
的操作步骤如下。
把数组
[1, 2, 7]
用 L 表示,[5, 8, 9]
用 R 表示。合并的时候,开辟分配一个新数组 T 保存结果,数组大小应该是两个子数组长度的总和
然后下标 i、j、k 分别指向每个数组的起始点。
接下来,比较下标 i 和 j 所指向的元素 L
[i]
和 R[j]
,按照大小顺序放入到下标 k 指向的地方,1 小于 5。移动 i 和 k,继续比较 L
[i]
和 R[j]
,2 比 5 小。i 和 k 继续往前移动,5 比 7 小。
移动 j 和 k,继续比较 L
[i]
和 R[j]
,7 比 8 小。这时候,左边的数组已经处理完毕,直接将右边数组剩余的元素放到结果数组里就好。
合并之所以能成功,先决条件必须是两个子数组都已经分别排好序了。
# 算法分析
# 空间复杂度
由于合并 n 个元素需要分配一个大小为 n 的额外数组,合并完成之后,这个数组的空间就会被释放,所以算法的空间复杂度就是 O(n)。归并排序也是稳定的排序算法。
# 时间复杂度
归并算法是一个不断递归的过程。
举例:数组的元素个数是 n,时间复杂度是 T(n) 的函数。
解法:把这个规模为 n 的问题分成两个规模分别为 n/2 的子问题,每个子问题的时间复杂度就是 T(n/2),那么两个子问题的复杂度就是 2×T(n/2)。当两个子问题都得到了解决,即两个子数组都排好了序,需要将它们合并,一共有 n 个元素,每次都要进行最多 n-1 次的比较,所以合并的复杂度是 O(n)。由此我们得到了递归复杂度公式: T(n) = 2×T(n/2) + O(n)。
对于公式求解,不断地把一个规模为 n 的问题分解成规模为 n/2 的问题,一直分解到规模大小为 1。如果 n 等于 2,只需要分一次;如果 n 等于 4,需要分 2 次。这里的次数是按照规模大小的变化分类的。
以此类推,对于规模为 n 的问题,一共要进行 log(n) 层的大小切分。在每一层里,我们都要进行合并,所涉及到的元素其实就是数组里的所有元素,因此,每一层的合并复杂度都是 O(n),所以整体的复杂度就是 O(nlogn)。
建议:归并算法的思想很重要,其中对两个有序数组合并的操作,在很多面试题里都有用到,建议大家一定要把这个算法练熟。
class BubbleSort extends Sort {
sort(originalArray) {
// Flag that holds info about whether the swap has occur or not.
let swapped = false;
// Clone original array to prevent its modification.
const array = [...originalArray];
for (let i = 1; i < array.length; i += 1) {
swapped = false;
// Call visiting callback.
this.callbacks.visitingCallback(array[i]);
for (let j = 0; j < array.length - i; j += 1) {
// Call visiting callback.
this.callbacks.visitingCallback(array[j]);
// Swap elements if they are in wrong order.
if (this.comparator.lessThan(array[j + 1], array[j])) {
[array[j], array[j + 1]] = [array[j + 1], array[j]];
// Register the swap.
swapped = true;
}
}
// If there were no swaps then array is already sorted and there is
// no need to proceed.
if (!swapped) {
return array;
}
}
return array;
}
}
/**
* 冒泡排序
* @param : <Array> target数组
* @description : 冒泡排序,更贴切的形容应该是沉底排序,每一轮内循环就是最大数沉底了。
*/
module.exports.bubbleSort = function bubbleSort(target) {
for (var j = target.length; j > 0; j--) {
for (var i = 0; i < j - 1; i++) {
if (target[i] > target[i + 1]) {
target.swap(i, i + 1);
}
}
}
return target;
};
# 5. 希尔排序 (缩小增量排序, 不稳定)
希尔排序:类似插入排序,但是对调的间隔扩大(插入排序是相邻元素对调位置的)。
按步长进行分组,组内直接插入,缩小增量再次进行此步骤,增量为 1 时相当于一次直接插入。
复杂度:最好 O(n) - 最坏 O(n^s 1<s<2) - 平均 O(n^1.3)
function sort(arr) {
const len = arr.length;
let h = 1;
while (h < parseInt(len / 3)) {
h = 3 * h + 1;
}
while (h >= 1) {
for (let i = h; i < len; i++) {
for (let j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
swap(arr, j, j - h);
console.log(arr);
}
}
h = parseInt(h / 3);
}
return arr;
}
function swap(arr, a, b) {
let x = arr[a];
arr[a] = arr[b];
arr[b] = x;
}
// 希尔排序
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12],
length = b.length;
// 从 start 开始,间隔为 gap 数组进行插入排序
function insertSort(a, start, gap) {
let temp;
for (let i = start; i < length; i++) {
for (let j = i + gap; j < length; j += gap) {
console.log('比较 ', i, j, a[i], a[j], a[j] < a[i] ? '交换' : '不交换');
if (a[j] < a[i]) {
temp = a[j];
a[j] = a[i];
a[i] = temp;
} else {
break;
}
}
}
return a;
}
function hillSort(a) {
for (let gap = parseInt(a.length / 2); gap >= 1; gap = parseInt(gap / 2)) {
console.log('gap', gap);
console.log('a', a);
insertSort(a, 0, gap);
}
return a;
}
console.log(hillSort(b));
import Sort from '../Sort';
export default class ShellSort extends Sort {
sort(originalArray) {
// Prevent original array from mutations.
const array = [...originalArray];
// Define a gap distance.
let gap = Math.floor(array.length / 2);
// Until gap is bigger then zero do elements comparisons and swaps.
while (gap > 0) {
// Go and compare all distant element pairs.
for (let i = 0; i < array.length - gap; i += 1) {
let currentIndex = i;
let gapShiftedIndex = i + gap;
while (currentIndex >= 0) {
// Call visiting callback.
this.callbacks.visitingCallback(array[currentIndex]);
// Compare and swap array elements if needed.
if (this.comparator.lessThan(array[gapShiftedIndex], array[currentIndex])) {
const tmp = array[currentIndex];
array[currentIndex] = array[gapShiftedIndex];
array[gapShiftedIndex] = tmp;
}
gapShiftedIndex = currentIndex;
currentIndex -= gap;
}
}
// Shrink the gap.
gap = Math.floor(gap / 2);
}
// Return sorted copy of an original array.
return array;
}
}
/**
* 希尔排序
* @param : <Array> target数组
* @description : 插入排序的改版:使用定好的偏移量分组,组内进行插入排序;减小偏移量重复进行分组排序直到偏移量为1.
*/
module.exports.shellSort = function shellSort(target) {
const len = target.length;
// 偏移量递减
for (let dx = Math.floor(len / 2); dx > 0; dx = Math.floor(dx / 2)) {
// 按偏移量分组[0,dx,2dx...], [1, 1+dx, 1+2dx...]
for (let i = 0; i < dx; i++) {
// 组内插入排序
for (let j = i + dx; j < len; j += dx) {
let k = j;
let base = target[k];
// 插入元素
while (k > i && base < target[k - dx]) {
target[k] = target[k - dx];
k -= dx;
}
if (k < j) {
target[k] = base;
}
}
}
}
return target;
};
# 6. 归并排序(稳定)
原理:两个有序序列的合并,方法:分治 + 递归
复杂度:最好 O(nlogn) - 最坏 O(nlogn) - 平均 O(nlogn)
归并排序:通过去中间值分割数组,然后再对子数组分割,分割到最小单位进行比较,然后逐个合并。还有一种(bottom to top)的方法是先将数组切分为若干段,然后进行合并排序。核心思想就是分而治之。
// 归并排序。分治思想,从数组中间开始将数组分成两个部分,然后分别对两个部分进行排序
// 效率为O(n log n)
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12],
length = b.length;
const mergeSort = arr => {
const length = arr.length;
if (length < 2) return arr;
const midIndex = parseInt(length / 2);
const left = arr.slice(0, midIndex),
right = arr.slice(midIndex, length);
return [...mergeSort(left), ...mergeSort(right)];
};
console.log(mergeSort(b));
优化版
let intensifyMergeArray = copyArray(arr);
function intensifyMerge(arr, l, mid, r) {
let i = l,
j = mid + 1,
newArr = arr.slice(l, r + 1);
for (let m = l; m <= r; m++) {
if (i > mid) {
arr[m] = newArr[j - l];
j++;
} else if (j > r) {
arr[m] = newArr[i - l];
i++;
} else if (newArr[i - l] < newArr[j - l]) {
arr[m] = newArr[i - l];
i++;
} else {
arr[m] = newArr[j - l];
j++;
}
}
}
function intensifyMergeEvent(arr, l, r) {
if (l >= r) return;
let mid = Math.floor((l + r) / 2);
intensifyMergeEvent(arr, l, mid);
intensifyMergeEvent(arr, mid + 1, r);
if (arr[mid] > arr[mid + 1]) {
intensifyMerge(arr, l, mid, r);
}
}
function intensifyMergeSort(arr) {
intensifyMergeEvent(arr, 0, arr.length - 1);
}
import Sort from '../Sort';
export default class MergeSort extends Sort {
sort(originalArray) {
// Call visiting callback.
this.callbacks.visitingCallback(null);
// If array is empty or consists of one element then return this array since it is sorted.
if (originalArray.length <= 1) {
return originalArray;
}
// Split array on two halves.
const middleIndex = Math.floor(originalArray.length / 2);
const leftArray = originalArray.slice(0, middleIndex);
const rightArray = originalArray.slice(middleIndex, originalArray.length);
// Sort two halves of split array
const leftSortedArray = this.sort(leftArray);
const rightSortedArray = this.sort(rightArray);
// Merge two sorted arrays into one.
return this.mergeSortedArrays(leftSortedArray, rightSortedArray);
}
mergeSortedArrays(leftArray, rightArray) {
let sortedArray = [];
// In case if arrays are not of size 1.
while (leftArray.length && rightArray.length) {
let minimumElement = null;
// Find minimum element of two arrays.
if (this.comparator.lessThanOrEqual(leftArray[0], rightArray[0])) {
minimumElement = leftArray.shift();
} else {
minimumElement = rightArray.shift();
}
// Call visiting callback.
this.callbacks.visitingCallback(minimumElement);
// Push the minimum element of two arrays to the sorted array.
sortedArray.push(minimumElement);
}
// If one of two array still have elements we need to just concatenate
// this element to the sorted array since it is already sorted.
if (leftArray.length) {
sortedArray = sortedArray.concat(leftArray);
}
if (rightArray.length) {
sortedArray = sortedArray.concat(rightArray);
}
return sortedArray;
}
}
// sort_merge_b2t
const arr = [];
const str = 'SORTEXAMPLE';
for (let char of str) {
arr.push(char);
}
let aux = new Array(arr.length);
function sort(arr) {
const len = arr.length;
aux = new Array(len);
for (let sz = 1; sz < len; sz = sz + sz) {
for (let lo = 0; lo < len - sz; lo += sz + sz) {
merge(arr, lo, lo + sz - 1, Math.min((len - 1, lo + sz + sz - 1)));
}
}
}
function merge(arr, lo, mid, hi) {
let i = lo,
j = mid + 1;
for (let k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (let k = lo; k <= hi; k++) {
if (i > mid) arr[k] = aux[j++];
else if (j > hi) arr[k] = aux[i++];
else if (aux[j] < aux[i]) arr[k] = aux[j++];
else arr[k] = aux[i++];
}
console.log(arr);
}
sort(arr, 0, arr.length - 1);
/*
这是一种先两两对比,然后44对比,如此往复。
最后对比整个数组。
["S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "S", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "S", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "S", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "S", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "S", "R", "T", "E", "X", "A", "M", "L", "P", "E"]
["O", "R", "S", "T", "E", "X", "A", "M", "L", "P", "E"]
["O", "R", "S", "T", "A", "E", "M", "X", "L", "P", "E"]
["O", "R", "S", "T", "A", "E", "M", "X", "E", "L", "P", undefined]
["A", "E", "M", "O", "R", "S", "T", "X", "E", "L", "P", undefined]
["A", "E", "E", "L", "M", "O", "P", "R", "S", "T", "X", undefined, undefined, undefined, undefined, undefined]
*/
// sort_merge_t2b
const arr = [];
const str = 'SORTEXAMPLE';
for (let char of str) {
arr.push(char);
}
let aux = new Array(arr.length);
function sort(arr, lo, hi) {
if (hi <= lo) return;
let mid = lo + parseInt((hi - lo) / 2);
sort(arr, lo, mid);
sort(arr, mid + 1, hi);
merge(arr, lo, mid, hi);
}
function merge(arr, lo, mid, hi) {
let i = lo,
j = mid + 1;
for (let k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (let k = lo; k <= hi; k++) {
if (i > mid) arr[k] = aux[j++];
else if (j > hi) arr[k] = aux[i++];
else if (aux[j] < aux[i]) arr[k] = aux[j++];
else arr[k] = aux[i++];
}
console.log(arr);
}
sort(arr, 0, arr.length - 1);
/*
通过归并法缩小范围
["S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
left
["O", "S", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "R", "S", "T", "E", "X", "A", "M", "P", "L", "E"]
["O", "R", "S", "E", "T", "X", "A", "M", "P", "L", "E"]
["O", "R", "S", "E", "T", "X", "A", "M", "P", "L", "E"]
["E", "O", "R", "S", "T", "X", "A", "M", "P", "L", "E"]
["E", "O", "R", "S", "T", "X", "A", "M", "P", "L", "E"]
right
["E", "O", "R", "S", "T", "X", "A", "M", "P", "L", "E"]
["E", "O", "R", "S", "T", "X", "A", "M", "P", "E", "L"]
["E", "O", "R", "S", "T", "X", "A", "E", "L", "M", "P"]
all
["A", "E", "E", "L", "M", "O", "P", "R", "S", "T", "X"]
*/
// 常见排序算法
/**
* 归并排序
* @param : <Array> target 要归并排序的数组
* @description : 归并排序,将数组递归分割成两个子数组直至数组只有一个元素,然后将这两个有序数组合并成一个有序数组;
*/
function mergeSortedArray(arrA, arrB) {
var result = [];
var i = 0,
j = 0,
targetLen = arrA.length,
toolLen = arrB.length;
while (i < targetLen && j < toolLen) {
if (arrA[i] < arrB[j]) {
result.push(arrA[i++]);
} else {
result.push(arrB[j++]);
}
}
while (i < targetLen) {
result.push(arrA[i++]);
}
while (j < toolLen) {
result.push(arrB[j++]);
}
return result;
}
module.exports.mergeSort = function mergeSort(target) {
if (target.length === 1) {
return target;
}
var mid = Math.floor(target.length / 2);
var left = target.slice(0, mid);
var right = target.slice(mid);
return mergeSortedArray(mergeSort(left), mergeSort(right));
};
# 7. 桶排序
// 桶排序. 值都是大于 0 的, 并且差不多都是均匀分布
// 桶排序的时间复杂度为 O(m+n)
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12],
length = b.length;
function bucketSort(a) {
// 找到最大和最小值
let min = (max = a[0]);
// O(n)
for (let i = 1; i < a.length; i++) {
if (min > a[i]) {
min = a[i];
}
if (max < a[i]) {
max = a[i];
}
}
let bucket = new Array(max + 1).fill(0);
for (let i = 0; i < a.length; i++) {
bucket[a[i]]++;
}
let result = [];
for (let i = 0; i < bucket.length; i++) {
if (typeof bucket[i] === 'number' && bucket[i] !== 0) {
result = [...result, ...new Array(bucket[i]).fill(i)];
}
}
return result;
}
console.log(bucketSort(b));
# 8. 基数排序(稳定)
原理:分配加收集
复杂度: O(d(n+r)) r 为基数 d 为位数 空间复杂度 O(n+r)
// 基数排序
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12],
length = b.length;
function insertSort(a) {
for (let i = 0; i < length - 1; i++) {
for (let j = i + 1; j < length; j++) {
if (a[i] > a[j]) {
const temp = a[i];
a[i] = a[j];
a[j] = temp;
}
console.log(a);
}
}
return a;
}
console.log(insertSort(b));
import Sort from '../Sort';
// Using charCode (a = 97, b = 98, etc), we can map characters to buckets from 0 - 25
const BASE_CHAR_CODE = 97;
const NUMBER_OF_POSSIBLE_DIGITS = 10;
const ENGLISH_ALPHABET_LENGTH = 26;
export default class RadixSort extends Sort {
/**
* @param {*[]} originalArray
* @return {*[]}
*/
sort(originalArray) {
// Assumes all elements of array are of the same type
const isArrayOfNumbers = this.isArrayOfNumbers(originalArray);
let sortedArray = [...originalArray];
const numPasses = this.determineNumPasses(sortedArray);
for (let currentIndex = 0; currentIndex < numPasses; currentIndex += 1) {
const buckets = isArrayOfNumbers
? this.placeElementsInNumberBuckets(sortedArray, currentIndex)
: this.placeElementsInCharacterBuckets(sortedArray, currentIndex, numPasses);
// Flatten buckets into sortedArray, and repeat at next index
sortedArray = buckets.reduce((acc, val) => {
return [...acc, ...val];
}, []);
}
return sortedArray;
}
/**
* @param {*[]} array
* @param {number} index
* @return {*[]}
*/
placeElementsInNumberBuckets(array, index) {
// See below. These are used to determine which digit to use for bucket allocation
const modded = 10 ** (index + 1);
const divided = 10 ** index;
const buckets = this.createBuckets(NUMBER_OF_POSSIBLE_DIGITS);
array.forEach(element => {
this.callbacks.visitingCallback(element);
if (element < divided) {
buckets[0].push(element);
} else {
/**
* Say we have element of 1,052 and are currently on index 1 (starting from 0). This means
* we want to use '5' as the bucket. `modded` would be 10 ** (1 + 1), which
* is 100. So we take 1,052 % 100 (52) and divide it by 10 (5.2) and floor it (5).
*/
const currentDigit = Math.floor((element % modded) / divided);
buckets[currentDigit].push(element);
}
});
return buckets;
}
/**
* @param {*[]} array
* @param {number} index
* @param {number} numPasses
* @return {*[]}
*/
placeElementsInCharacterBuckets(array, index, numPasses) {
const buckets = this.createBuckets(ENGLISH_ALPHABET_LENGTH);
array.forEach(element => {
this.callbacks.visitingCallback(element);
const currentBucket = this.getCharCodeOfElementAtIndex(element, index, numPasses);
buckets[currentBucket].push(element);
});
return buckets;
}
/**
* @param {string} element
* @param {number} index
* @param {number} numPasses
* @return {number}
*/
getCharCodeOfElementAtIndex(element, index, numPasses) {
// Place element in last bucket if not ready to organize
if (numPasses - index > element.length) {
return ENGLISH_ALPHABET_LENGTH - 1;
}
/**
* If each character has been organized, use first character to determine bucket,
* otherwise iterate backwards through element
*/
const charPos = index > element.length - 1 ? 0 : element.length - index - 1;
return element.toLowerCase().charCodeAt(charPos) - BASE_CHAR_CODE;
}
/**
* Number of passes is determined by the length of the longest element in the array.
* For integers, this log10(num), and for strings, this would be the length of the string.
*/
determineNumPasses(array) {
return this.getLengthOfLongestElement(array);
}
/**
* @param {*[]} array
* @return {number}
*/
getLengthOfLongestElement(array) {
if (this.isArrayOfNumbers(array)) {
return Math.floor(Math.log10(Math.max(...array))) + 1;
}
return array.reduce((acc, val) => {
return val.length > acc ? val.length : acc;
}, -Infinity);
}
/**
* @param {*[]} array
* @return {boolean}
*/
isArrayOfNumbers(array) {
// Assumes all elements of array are of the same type
return this.isNumber(array[0]);
}
/**
* @param {number} numBuckets
* @return {*[]}
*/
createBuckets(numBuckets) {
/**
* Mapping buckets to an array instead of filling them with
* an array prevents each bucket from containing a reference to the same array
*/
return new Array(numBuckets).fill(null).map(() => []);
}
/**
* @param {*} element
* @return {boolean}
*/
isNumber(element) {
return Number.isInteger(element);
}
}
# 9. 计数排序
// 计数排序
const b = [3, 4, 6, 1, 3, 6, 32, 45, 21, 12];
function insertSort(a) {
let min = (max = 0);
for (let i = 0; i < a.length - 1; i++) {
for (let j = i + 1; j < a.length; j++) {
if (a[j] > a[i]) {
max = j;
} else {
min = i;
}
}
}
console.log('max', max, a[max]);
console.log('min', min, a[min]);
return a;
}
console.log(insertSort(b));
/**
* Assumes each of the n input elements is an integer in the range 0 --> k,
* for some integer k. If k = O(n), then CountingSort runs in ϴ(n) time
*
* Notes:
* - Requires no user input of min or max
* - Supports negative numbers
*
* @complexity: O(k) where k is the range
* @flow
*/
export default function CountingSort(elements) {
let z = 0;
let max = elements[0];
let min = elements[0];
for (let i = 1; i < elements.length; i++) {
max = Math.max(max, elements[i]);
min = Math.min(min, elements[i]);
}
const range = max - min;
const finalArr = new Array(elements.length).fill(0);
// Below is where algorithm may be inefficient (when range is too large)
const countArr = new Array(range + 1 || 0).fill(0);
for (let i = 0; i < elements.length; i++) {
countArr[elements[i] - min]++;
}
for (let i = 0; i <= range; i++) {
while (countArr[i]-- > 0) {
finalArr[z++] = i + min;
}
}
return finalArr;
}
import Sort from '../Sort';
export default class CountingSort extends Sort {
/**
* @param {number[]} originalArray
* @param {number} [smallestElement]
* @param {number} [biggestElement]
*/
sort(originalArray, smallestElement = undefined, biggestElement = undefined) {
// Init biggest and smallest elements in array in order to build number bucket array later.
let detectedSmallestElement = smallestElement || 0;
let detectedBiggestElement = biggestElement || 0;
if (smallestElement === undefined || biggestElement === undefined) {
originalArray.forEach(element => {
// Visit element.
this.callbacks.visitingCallback(element);
// Detect biggest element.
if (this.comparator.greaterThan(element, detectedBiggestElement)) {
detectedBiggestElement = element;
}
// Detect smallest element.
if (this.comparator.lessThan(element, detectedSmallestElement)) {
detectedSmallestElement = element;
}
});
}
// Init buckets array.
// This array will hold frequency of each number from originalArray.
const buckets = Array(detectedBiggestElement - detectedSmallestElement + 1).fill(0);
originalArray.forEach(element => {
// Visit element.
this.callbacks.visitingCallback(element);
buckets[element - detectedSmallestElement] += 1;
});
// Add previous frequencies to the current one for each number in bucket
// to detect how many numbers less then current one should be standing to
// the left of current one.
for (let bucketIndex = 1; bucketIndex < buckets.length; bucketIndex += 1) {
buckets[bucketIndex] += buckets[bucketIndex - 1];
}
// Now let's shift frequencies to the right so that they show correct numbers.
// I.e. if we won't shift right than the value of buckets[5] will display how many
// elements less than 5 should be placed to the left of 5 in sorted array
// INCLUDING 5th. After shifting though this number will not include 5th anymore.
buckets.pop();
buckets.unshift(0);
// Now let's assemble sorted array.
const sortedArray = Array(originalArray.length).fill(null);
for (let elementIndex = 0; elementIndex < originalArray.length; elementIndex += 1) {
// Get the element that we want to put into correct sorted position.
const element = originalArray[elementIndex];
// Visit element.
this.callbacks.visitingCallback(element);
// Get correct position of this element in sorted array.
const elementSortedPosition = buckets[element - detectedSmallestElement];
// Put element into correct position in sorted array.
sortedArray[elementSortedPosition] = element;
// Increase position of current element in the bucket for future correct placements.
buckets[element - detectedSmallestElement] += 1;
}
// Return sorted array.
return sortedArray;
}
}
# 10. 堆排序
堆排序利用了二叉堆的特性来做,二叉堆通常用数组表示,并且二叉堆是一颗完全二叉树(所有叶节点(最底层的节点)都是从左往右顺序排序,并且其他层的节点都是满的)。二叉堆又分为大根堆与小根堆。
- 大根堆是某个节点的所有子节点的值都比他小
- 小根堆是某个节点的所有子节点的值都比他大
堆排序的原理就是组成一个大根堆或者小根堆。以小根堆为例,某个节点的左边子节点索引是 i * 2 + 1
,右边是 i * 2 + 2
,父节点是 (i - 1) /2
。
- 首先遍历数组,判断该节点的父节点是否比他小,如果小就交换位置并继续判断,直到他的父节点比他大
- 重新以上操作 1,直到数组首位是最大值
- 然后将首位和末尾交换位置并将数组长度减一,表示数组末尾已是最大值,不需要再比较大小
- 对比左右节点哪个大,然后记住大的节点的索引并且和父节点对比大小,如果子节点大就交换位置
- 重复以上操作 3 - 4 直到整个数组都是大根堆。
原理:利用堆的特性。堆排序的思想就是堆的根肯定是最大的。
- 把最大的与最后一个元素交换
- 除最后一个元素外, 对根节点进行一次堆重组(heapify)
- 重复 1 和 2
复杂度:平均 - 最好 - 最坏 都是 O(nlogn)
function heapSort(tree, n) {
// 先把一个数组组成一个堆
buildHeap(tree, n);
// 从最后一个节点开始
for (let i = n - 1; i >= 0; i--) {
swap(tree, i, 0);
// 1.把最大的与最后一个元素交换
heapify(tree, i, 0);
}
}
// 根据数组创建一个堆
function buildHeap(tree, n) {
// n = tree.length
// last_node = tree.length - 1 也就是下标
let last_node = n - 1;
// 节点 i 的父节点的索引 i = (i - 1) / 2
let parent = (last_node - 1) / 2;
// 从最后一个父节点进行堆重组
for (let i = parent; i >= 0; i--) {
heapify(tree, n, i);
}
}
// 交换两个元素的值
function swap(tree, max, i) {
let temp = tree[i];
tree[i] = tree[max];
tree[max] = temp;
}
// 对第 i 个节点进行堆重组,n 为数组 tree 元素的个数
function heapify(tree, n, i) {
// 递归出口
if (i >= n) {
return;
}
// 左孩子节点索引
let c1 = 2 * i + 1;
// 右孩子节点索引
let c2 = 2 * i + 2;
// 最大节点索引。 也就是根节点
let max = i;
if (c1 < n && tree[c1] > tree[max]) {
max = c1;
}
if (c2 < n && tree[c2] > tree[max]) {
max = c2;
}
// 说明不是 大根堆(每个节点的值都大于左右孩子节点),需要对其子树进行一次堆重组
if (max != i) {
// 交换 i 与 max 对应的值
swap(tree, max, i);
// 继续对子树进行堆重组
heapify(tree, n, max);
}
}
const arr = [1, 2, 3, 11, 13, 12, 9, 8, 10, 15, 14, 7];
heapSort(arr, arr.length);
console.log(arr);
堆排序:利用优先队列可以方便查找最大值的特点,逐个求出最大值,从右到左、从大到小排序。
import MaxHeap from '../DataStructures/MaxHeap';
export default function HeapSort(items: Array<number>) {
const heap = new MaxHeap();
const list = [];
// Insert every element in the node into the heap. Calling
// insert() will maxHeapify()
items.forEach(item => {
heap.insert(item);
});
// For every element in the heap, delete the max value.
// Right after deletion, the MaxHeap will automatically propogate
// the next highest value of the heap to the root
heap.get().forEach(() => {
// Delete the roo
const node = heap.deleteNodeIndex(0);
list.push(node);
});
return list.reverse();
}
const pq = [];
let N = 0;
function less(i, j) {
return pq[i] < pq[j];
}
function swap(i, j) {
let tmp = pq[i];
pq[i] = pq[j];
pq[j] = tmp;
}
// 上浮
function swim(k) {
while (k > 1 && less(parseInt(k / 2), k)) {
swap(parseInt(k / 2), k);
k = parseInt(k / 2);
}
}
// 下沉
function sink(k) {
while (2 * k <= N) {
let j = 2 * k;
if ((j < N) & less(j, j + 1)) j++;
if (!less(k, j)) break;
swap(k, j);
k = j;
}
}
function insert(v) {
pq[++N] = v;
swim(N);
console.log(pq);
}
function deleteMax() {
let max = pq[1];
swap(1, N--);
pq[N + 1] = null;
sink(1);
console.log(pq);
return max;
}
insert('P');
insert('Q');
insert('E');
deleteMax();
insert('X');
insert('A');
insert('M');
deleteMax();
insert('P');
insert('L');
insert('E');
deleteMax();
/**
* 堆排序
* 将数组变为二叉字符树
* 通过操作字符数的上浮和下沉来完成两个功能
* 1. 删除并返回最大值
* 2. 插入并求出最大值
*/
/*
[empty, "P"]
[empty, "Q", "P"]
[empty, "Q", "P", "E"]
[empty, "P", "E", null]
[empty, "X", "E", "P"]
[empty, "X", "E", "P", "A"]
[empty, "X", "M", "P", "A", "E"]
[empty, "P", "M", "E", "A", null]
[empty, "P", "P", "E", "A", "M"]
[empty, "P", "P", "L", "A", "M", "E"]
[empty, "P", "P", "L", "A", "M", "E", "E"]
[empty, "P", "M", "L", "A", "E", "E", null]
*/
// 上浮
// function swim(k) {
// while (k > 1 && less(parseInt(k / 2), k)) {
// swap(parseInt(k / 2), k)
// k = parseInt(k / 2)
// }
// }
// 下沉
function sink(arr, k, len) {
while (2 * k <= len) {
let j = 2 * k;
if ((j < len) & (arr[j] < arr[j + 1])) j++;
if (arr[k] >= arr[j]) break;
swap(arr, k, j);
k = j;
}
console.log('sink', arr);
}
function sort(arr) {
let len = arr.length;
console.log('step 01');
for (let k = parseInt(len / 2); k >= 0; k--) {
sink(arr, k, len);
}
len--;
console.log('step 02');
while (len > 0) {
swap(arr, 0, len--);
sink(arr, 0, len);
}
console.log(arr);
}
function swap(arr, a, b) {
let x = arr[a];
arr[a] = arr[b];
arr[b] = x;
}
const arr = ['S', 'O', 'R', 'T', 'E', 'X', 'A', 'M', 'P', 'L', 'E'];
sort(arr);
/**
* 堆排序
* 先将数组进行堆有序化
* 然后求出最大值移动到最后
* 然后将堆的范围向前缩小一位
*/
/*
step 01
["S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "E"]
["S", "O", "R", "T", "P", "X", "A", "M", "E", "L", "E"]
["S", "O", "R", "T", "P", "X", "A", "M", "E", "L", "E"]
["S", "O", "X", "T", "P", "R", "A", "M", "E", "L", "E"]
["S", "X", "R", "T", "P", "O", "A", "M", "E", "L", "E"]
["X", "T", "R", "S", "P", "O", "A", "M", "E", "L", "E"]
step 02
["T", "S", "R", "M", "P", "O", "A", "E", "E", "L", "X"]
["S", "R", "P", "M", "L", "O", "A", "E", "E", "T", "X"]
["R", "P", "O", "M", "L", "E", "A", "E", "S", "T", "X"]
["P", "O", "L", "M", "E", "E", "A", "R", "S", "T", "X"]
["O", "M", "L", "A", "E", "E", "P", "R", "S", "T", "X"]
["M", "L", "E", "A", "E", "O", "P", "R", "S", "T", "X"]
["L", "E", "E", "A", "M", "O", "P", "R", "S", "T", "X"]
["E", "E", "A", "L", "M", "O", "P", "R", "S", "T", "X"]
["E", "A", "E", "L", "M", "O", "P", "R", "S", "T", "X"]
["A", "E", "E", "L", "M", "O", "P", "R", "S", "T", "X"]
*/
/**
* 堆排序
* @param : <Array> target
* @description : 通过构建大(小)根堆的方式进行排序,PS:使用函数副作用来进行原地排序
*/
// 递归调整 i~j 层的大根堆
function adjustMaxHeap(target, i, j) {
let parent = i;
let left = 2 * i + 1;
let right = 2 * i + 2;
// 比较父节点与左右叶子节点,取最大值的下标设为父节点下标
if (left < j && target[parent] < target[left]) {
parent = left;
}
if (right < j && target[parent] < target[right]) {
parent = right;
}
// 只有父节点发生改变才会破坏大根堆结构,此时才需要继续调整下级大根堆
if (parent != i) {
target.swap(i, parent);
adjustMaxHeap(target, parent, j);
}
}
// 构建大根堆就是不断调整最大堆的过程,只要从最后一个父节点往上调整到第一个父节点,就能构建出大根堆
// 从0开始的n层堆的结构:len = 2^n - 1,第n层全是叶子,所以第n-1层的最后一个父节点就是floor(len/2)-1
function buildMaxHeap(target) {
const len = target.length;
for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
adjustMaxHeap(target, i, len);
}
}
function sortMaxHeap(target) {
for (let i = target.length - 1; i > 0; i--) {
target.swap(0, i);
adjustMaxHeap(target, 0, i);
}
}
// 先构建一个大根堆,然后从最后一个元素开始交换堆顶元素,每次交换都调整根堆,直到数组头则完成排序
module.exports.heapSort = function heapSort(target) {
buildMaxHeap(target);
sortMaxHeap(target);
return target;
};
/**
* 堆排序提取部分记录
* 从大数据中提取最大(小)的n条记录,也可以用小(大)根堆来实现
* 先用数据集中前n条数据构造一个小根堆,然后将后面的数据依次通过这个小根堆:
* 比堆顶小的数据直接丢弃,比堆顶大则替换堆顶,然后调整根堆。最后输出小根堆的排序
*/
module.exports.topSortViaHeap = function topSortViaHeap(target) {
const len = 10;
let ret = target.slice(0, len);
buildMaxHeap(ret);
for (var i = len; i < target.length; i++) {
if (target[i] < ret[0]) {
ret[0] = target[i];
adjustMaxHeap(ret, 0, ret.length);
}
}
sortMaxHeap(ret);
return ret;
};
# 堆排序实战题
在 100000 个浮点数中找出最大的 1000 个数,时间复杂度最优 (用 堆排序 O(nlogn))
# 二分搜索树
堆排序 应用:用尺寸为 n 的最小堆(最大堆)来筛选大数据集里面最大(小)的 n 个数 流程:
- 先使用数据集里面的前 n 条数据来构造最小堆
- 遍历数据集,将每个数据与堆顶元素比较,若小于堆顶则抛弃,否则替换掉堆顶
- 调整替换后的堆,继续 2
- 用有序数组+2 分查找也是类似的,不过效率低一些。
# 11. V8 的排序
对于 JS 来说,数组长度大于 10 会采用快排,否则使用插入排序 源码实现 (opens new window) 。选择插入排序是因为虽然时间复杂度很差,但是在数据量很小的情况下和 O(N * logN)
相差无几,然而插入排序需要的常数时间很小,所以相对别的排序来说更快。
# 12. 拓扑排序 (Topological Sort)
# 基本思想
和前面介绍的几种排序不同,拓扑排序应用的场合不再是一个简单的数组,而是研究图论里面顶点和顶点连线之间的性质。拓扑排序就是要将这些顶点按照相连的性质进行排序。
要能实现拓扑排序,得有几个前提:
图必须是有向图
图里面没有环
拓扑排序一般用来理清具有依赖关系的任务。
举例:假设有三门课程 A、B、C,如果想要学习课程 C 就必须先把课程 B 学完,要学习课程 B,还得先学习课程 A,所以得出课程的学习顺序应该是 A -> B -> C。
# 实现
将问题用一个有向无环图(DAG, Directed Acyclic Graph)进行抽象表达,定义出哪些是图的顶点,顶点之间如何互相关联。
可以利用广度优先搜索或深度优先搜索来进行拓扑排序。
# 例题分析
有一个学生想要修完 5 门课程的学分,这 5 门课程分别用 1、2、3、4、5 来表示,现在已知学习这些课程有如下的要求:
课程 2 和 4 依赖于课程 1
课程 3 依赖于课程 2 和 4
课程 4 依赖于课程 1 和 2
课程 5 依赖于课程 3 和 4
那么这个学生应该按照怎样的顺序来学习这 5 门课程呢?
# 解题思路
可以把 5 门课程看成是一个图里的 5 个顶点,用有向线段按照它们的相互关系连起来,于是得出下面的有向图。
首先可以看到,这个有向图里没有环,无论从哪个顶点出发,都不会再回到那个顶点。并且,这个图里并没有孤岛的出现,因此,我们可以对它进行拓扑排序。
方法就是,一开始的时候,对每个顶点统计它们各自的前驱(也就是入度):1(0),2(1),3(2),4(1),5(2)。
选择其中一个没有前驱(也就是入度为 0)的顶点,在这道题里面,顶点 1 就是我们要找的那个点,将它作为结果输出。同时删除掉该顶点和所有以它作为起始点的有向边,更新顶点的入度表。
接下来,顶点 2 就是下一个没有前驱的顶点,输出顶点 2,并将以它作为起点的有向边删除,同时更新入度表。
再来,顶点 4 成为了没有前驱的顶点,输出顶点 4,删除掉它和顶点 3 和 5 的有向边。
然后,顶点 3 没有了前驱,输出它,并删除它与 5 的有向边。
最后,顶点 5 没有前驱,输出它,于是得出最后的结果为:1,2,4,3,5。
一般来说,一个有向无环图可以有一个或多个拓扑排序的序列。
# 代码示例
运用广度优先搜索的方法对这个图的结构进行遍历。在构建这个图的过程中,用一个链接矩阵 adj 来表示这个图的结构,用一个 indegree 的数组统计每个顶点的入度,重点看如何实现拓扑排序。
void sort() {
Queue
// 将所有入度为 0 的顶点加入到队列 q
for (int v = 0; v < V; v++) {
if (indegree`[v]` == 0) q.add(v);
}
// 循环,直到队列为空
while (!q.isEmpty()) {
int v = q.poll();
// 每次循环中,从队列中取出顶点,即为按照入度数目排序中最小的那个顶点
print(v);
// 将跟这个顶点相连的其他顶点的入度减 1,如果发现那个顶点的入度变成了 0,将其加入到队列的末尾
for (int u = 0; u < adj`[v]`.length; u++) {
if (\-\-indegree`[u]` == 0) {
q.add(u);
}
}
}
}
# 算法分析
# 时间复杂度
统计顶点的入度需要 O(n) 的时间,接下来每个顶点被遍历一次,同样需要 O(n) 的时间,所以拓扑排序的时间复杂度是 O(n)。
import Sort from '../Sort';
export default class QuickSort extends Sort {
/**
* @param {*[]} originalArray
* @return {*[]}
*/
sort(originalArray) {
// Clone original array to prevent it from modification.
const array = [...originalArray];
// If array has less than or equal to one elements then it is already sorted.
if (array.length <= 1) {
return array;
}
// Init left and right arrays.
const leftArray = [];
const rightArray = [];
// Take the first element of array as a pivot.
const pivotElement = array.shift();
const centerArray = [pivotElement];
// Split all array elements between left, center and right arrays.
while (array.length) {
const currentElement = array.shift();
// Call visiting callback.
this.callbacks.visitingCallback(currentElement);
if (this.comparator.equal(currentElement, pivotElement)) {
centerArray.push(currentElement);
} else if (this.comparator.lessThan(currentElement, pivotElement)) {
leftArray.push(currentElement);
} else {
rightArray.push(currentElement);
}
}
// Sort left and right arrays.
const leftArraySorted = this.sort(leftArray);
const rightArraySorted = this.sort(rightArray);
// Let's now join sorted left array with center array and with sorted right array.
return leftArraySorted.concat(centerArray, rightArraySorted);
}
}
import Sort from '../Sort';
export default class QuickSortInPlace extends Sort {
/** Sorting in place avoids unnecessary use of additional memory, but modifies input array.
*
* This process is difficult to describe, but much clearer with a visualization:
* @see: http://www.algomation.com/algorithm/quick-sort-visualization
*
* @param {*[]} originalArray - Not sorted array.
* @param {number} inputLowIndex
* @param {number} inputHighIndex
* @param {boolean} recursiveCall
* @return {*[]} - Sorted array.
*/
sort(originalArray, inputLowIndex = 0, inputHighIndex = originalArray.length - 1, recursiveCall = false) {
// Copies array on initial call, and then sorts in place.
const array = recursiveCall ? originalArray : [...originalArray];
/**
* The partitionArray() operates on the subarray between lowIndex and highIndex, inclusive.
* It arbitrarily chooses the last element in the subarray as the pivot.
* Then, it partially sorts the subarray into elements than are less than the pivot,
* and elements that are greater than or equal to the pivot.
* Each time partitionArray() is executed, the pivot element is in its final sorted position.
*
* @param {number} lowIndex
* @param {number} highIndex
* @return {number}
*/
const partitionArray = (lowIndex, highIndex) => {
/**
* Swaps two elements in array.
* @param {number} leftIndex
* @param {number} rightIndex
*/
const swap = (leftIndex, rightIndex) => {
const temp = array[leftIndex];
array[leftIndex] = array[rightIndex];
array[rightIndex] = temp;
};
const pivot = array[highIndex];
// visitingCallback is used for time-complexity analysis.
this.callbacks.visitingCallback(pivot);
let partitionIndex = lowIndex;
for (let currentIndex = lowIndex; currentIndex < highIndex; currentIndex += 1) {
if (this.comparator.lessThan(array[currentIndex], pivot)) {
swap(partitionIndex, currentIndex);
partitionIndex += 1;
}
}
// The element at the partitionIndex is guaranteed to be greater than or equal to pivot.
// All elements to the left of partitionIndex are guaranteed to be less than pivot.
// Swapping the pivot with the partitionIndex therefore places the pivot in its
// final sorted position.
swap(partitionIndex, highIndex);
return partitionIndex;
};
// Base case is when low and high converge.
if (inputLowIndex < inputHighIndex) {
const partitionIndex = partitionArray(inputLowIndex, inputHighIndex);
const RECURSIVE_CALL = true;
this.sort(array, inputLowIndex, partitionIndex - 1, RECURSIVE_CALL);
this.sort(array, partitionIndex + 1, inputHighIndex, RECURSIVE_CALL);
}
return array;
}
}
"快速排序"的思想很简单,整个排序过程只需要三步:
- 在数据集之中,找一个基准点
- 建立两个数组,分别存储左边和右边的数组
- 利用递归进行下次比较
function quickSort(arr) {
if (arr.length <= 1) {
return arr; //如果数组只有一个数,就直接返回;
}
var num = Math.floor(arr.length / 2); //找到中间数的索引值,如果是浮点数,则向下取整
var numValue = arr.splice(num, 1); //找到中间数的值
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++) {
if (arr[i] < numValue) {
left.push(arr[i]); //基准点的左边的数传到左边数组
} else {
right.push(arr[i]); //基准点的右边的数传到右边数组
}
}
return quickSort(left).concat([numValue], quickSort(right)); //递归不断重复比较
}
alert(quickSort([32, 45, 37, 16, 2, 87])); //弹出“2,16,32,37,45,87”
/**
* 快速排序
* @param : <Array> target数组
* @description : 选择一个元素将数组分隔成两部分,比该元素小的放该元素前面,比该元素大放后面;
* 然后递归快速排序,最终得到一个排序后数组
*/
module.exports.quickSort = function quickSort(target) {
// 先定义递归终止条件
if (target.length < 2) {
return target;
}
var baseIndex = 0;
var left = [];
var right = [];
for (var i = 1; i < target.length; i++) {
if (target[i] < target[baseIndex]) {
left.push(target[i]);
} else {
right.push(target[i]);
}
}
left = quickSort(left);
right = quickSort(right);
return left.concat(target[baseIndex], right); // 递归出口
};
/**
* 原地快速排序
* @param : <Array> target
* @description : 上面的快排每次都开辟一个数组,浪费空间。常规做法是两边查找到中间,两两交换位置
*/
function _inPlaceQuickSort(target, left, right) {
// 先定义递归终止条件
if (left >= right) {
return target;
}
var base = target[left];
var i = left;
var j = right;
while (i < j) {
while (i < j && target[j] >= base) {
j--;
}
target[i] = target[j];
while (i < j && target[i] <= base) {
i++;
}
target[j] = target[i];
}
target[i] = base;
// 函数副作用已经改变了传入数组,但是用显式返回看起来更清晰
target = _inPlaceQuickSort(target, left, i - 1);
target = _inPlaceQuickSort(target, i + 1, right);
return target;
}
module.exports.inPlaceQuickSort = function inPlaceQuickSort(target) {
return _inPlaceQuickSort(target, 0, target.length - 1);
};
# Other
function sort(array) {
if (array.length < 2) {
return array;
}
let mid = Math.floor(array.length / 2);
let left = array.slice(0, mid);
let right = array.slice(mid);
return merge(sort(left), sort(right));
}
function merge(left, right) {
let result = [];
let indexLeft = 0;
let indexRight = 0;
while (indexLeft < left.length && indexRight < right.length) {
result.push(left[indexLeft] < right[indexRight] ? left[indexLeft++] : right[indexRight++]);
}
return result.concat(left.slice(indexLeft)).concat(right.slice(indexRight));
}