十大排序算法
复杂度:
不稳定的排序:“快选希堆”
1. 直接插入排序
枚举一个元素分别与前面的元素比较,直到遇到比自己更小的
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) {
let tmp = arr[i];
for (let j = i; j >= 0; j--) {
/* 前面的值还是大于tmp则继续向前找 */
if (tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
} else {
arr[j] = tmp; break;
}
}
}
}
}
2. 希尔排序
原理:将数组不断分块,将块与块的对应元素进行比较后排序,重复进行直到每块只含有一个数,相当于间隔不为1的直接插入排序
时间复杂度:平均O(n1.3),已经排好序的情况下O(n),最差O(n2);
空间复杂度:常数级空间O(1)
稳定性:由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以,Shell排序是不稳定的。
function shellSort(arr) {
let len = Math.floor(arr.length / 2);
while (len) {
/* len 2*len 3*len ... */
for (let i = len; i < arr.length; i += len) {
if (arr[i] < arr[i - len]) {
let tmp = arr[i];
/* 查找前面可以插入的位置 */
for (let j = i; j >= 0; j -= len) {
if (tmp < arr[j - len]) {
arr[j] = arr[j - len];//后移一位
} else {
arr[j] = tmp; break;//找到可赋值的位置
}
}
}
}
/* 折半 */
len = Math.floor(len / 2);
}
}
/* 测试 */
let arr = [1, 39, 3, 2, 43, 23, 55];
shellSort(arr);
console.log(arr);// [1, 2, 3, 23, 39, 43, 55]
3. 简单选择排序
每次选出后面最小值放到当前位置
function selectSort(arr) {
for (let i = 0; i < arr.length; i++) {
let k = i;
// 找出后面最小值
for (let j = i + 1; j < arr.length; j++)
if (arr[j] < arr[k]) k = j;
//交换
[arr[k], arr[i]] = [arr[i], arr[k]];
}
}
4. 堆排序
原理:构建大顶堆,将堆顶放到数组最后,每次减少数组中用来建堆的元素个数
实现方法:
- 对数组区间
[0,n]
构建大顶堆,此时父元素都比子元素大 - 将第一个元素与最后一个元素互换,最后一个就是最大的
- 堆的元素个数减少1,更新第一个元素下的堆,形成新的大顶堆
- 重复2-3步骤,直到堆的元素个数为0
时间复杂度:初始化执行n/2次heapify
,更新执行n-1次,每次heapfy最多更新logn次(堆的高度),时间复杂度O(nlogn)
空间复杂度:所需空间主要用于递归,为O(logn)
稳定性:在堆顶与堆尾交换的时候两个相等的记录在序列中的相对位置就可能发生改变,所以堆排序是不稳定的
讲解视频:堆排序
/* 负责构建和更新堆 */
function heapify(arr, root, n) {
let max = root,
left = root * 2 + 1,
right = root * 2 + 2;
if (left < n && arr[left] > arr[max])
max = left;
if (right < n && arr[right] > arr[max])
max = right;
/* 存在子元素大于root则更新 */
if (root !== max) {
//交换值并更新max下的堆
[arr[root], arr[max]] = [arr[max], arr[root]];
heapify(arr, max, n);
}
}
/* 堆排序 */
function heapSort(arr) {
let n = arr.length;
/* 初始化,对含有子节点的节点构建*/
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, i, n);
}
/* 首尾交换 */
[arr[0], arr[n - 1]] = [arr[n - 1], arr[0]];
/* 更新根被替换的堆 */
for (let i = n - 1; i > 0; i--) {
//每次替换的都是头节点,所以更新头节点,长度为i
heapify(arr, 0, i);
[arr[0], arr[i - 1]] = [arr[i - 1], arr[0]];
}
}
/* 测试 */
let arr = [1, 39, 3, 2, 43, 23, 55];
heapSort(arr);
console.log(arr);// [1, 2, 3, 23, 39, 43, 55]
5. 冒泡排序
每轮将最大的数移动到最后即可
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
/* 对[0,n-i-1]区域进行排序 */
for (let j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
/* 交换 */
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
}
6. 快速排序
方法1:
- 每次找一个值
p
作为参考,设置左右指针i,j
从两边往中间遍历 - 若左边
i
位置遇到小于参考值停止遍历,右边j
位置遇到大于参考值停止遍历 - 两个停止的位置
i,j
进行交换数值 - 再次进行同样的步骤,最终左右指针的交点
i==j
就是参考值的最终位置 - 将参考值与交点互换数值,然后对交点左边区域
[start,i-1]
和右边区域[i+1,end]
分别进行快速排序
方法2:
- 每次找一个值
p
作为参考值,设置i指针记录第一个大于等于p
的位置 - 设置值
j
遍历数组,当遇到小于参考值p
的位置时,将第一个大于等于p
的值与arr[j]
互换,i
指向下一个位置 - 最终将
i
位置与p
位置对应值交换,再对两边区域进行快速排序
可以看出快速排序一般情况下分为logn
层,平均时间复杂度O(nlogn)
,最坏是逆序的时候递归n
层时间复杂度O(n^2)
,空间复杂度主要为logn
层递归占用内存O(logn)
,最坏情况O(n)
,由于关键字的比较是跳跃性的,所以快速排序不稳定
/* 方法1:从两边扫描 */
function quickSort(arr, start, end) {
if (start >= end) return;
let i = start, j = end;
let p = arr[end];
while (i < j) {
while (i < j && arr[i] < p) i++;
while (j > i && arr[j] >= p) j--;
/* 交换i,j位置的值 */
[arr[j], arr[i]] = [arr[i], arr[j]];
}
/* 将p移动到最终位置 */
[arr[end], arr[i]] = [arr[i], arr[end]];
quickSort(arr, start, i - 1);
quickSort(arr, i + 1, end);
}
/* 方法2:一个指针负责遍历,另一个负责第一个大于等于p的位置 */
function quickSort(arr, start, end) {
if (start >= end) return;
let p = arr[end];
let i = start;
for (let j = start; j < end; j++) {
// 遇到小于p的就放到前面,并且个数+1
if (arr[j] < p) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;//此时[start,i-1]内的元素都小于p
}
}
[arr[i], arr[end]] = [arr[end], arr[i]];
quickSort(arr, start, i - 1);
quickSort(arr, i + 1, end);
}
/* 测试 */
let arr = [1, 39, 3, 2, 43, 23, 55]
quickSort(arr, 0, arr.length - 1);
console.log(arr);//[ 1, 2, 3, 23, 39, 43, 55 ]
7. 归并排序
将长度为len
的数组一直二分至长度为1的多个数组,然后进行自下而上排序合并
从上图可以看出归并排序层数logn+1,每一层合并的代价是n,时间复杂度O(nlogn),额外空间就是那个辅助数组,所以空间复杂度O(n),在合并时如果相等则把前面的数放进辅助数组,所以是稳定的
function merge(arr, start, mid, end) {
let res = [];
let i = start, j = mid + 1;
/* 比较,将小的值放到数组 */
while (i <= mid && j <= end) {
if (arr[i] <= arr[j])
res.push(arr[i++]);
else
res.push(arr[j++]);
}
/* 剩余的直接赋值 */
while (i <= mid) res.push(arr[i++]);
while (j <= end) res.push(arr[j++]);
/* 赋值到arr */
for (let i = 0; i <= end - start; i++) {
arr[start + i] = res[i];
}
}
function mergeSort(arr, start, end) {
if (start >= end) return;
else {
/* 求中间值,分为两段进行处理 */
let mid = Math.floor(start + end >> 1);
mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
/* 将处理结果合并 */
merge(arr, start, mid, end);
}
}
let arr = [1, 39, 3, 2, 43, 23, 55]
mergeSort(arr, 0, arr.length - 1);
console.log(arr);//[ 1, 2, 3, 23, 39, 43, 55 ]
8. 计数排序
不基于元素比较,利用数组下标来确定元素的正确位置。
function countSort(arr) {
let max = 0, count = [];
for (let i = 0; i < arr.length; i++) {
max = arr[i] > max ? arr[i] : max;
count[arr[i]] = count[arr[i]] ? (count[arr[i]] + 1) : 1;
}
let k = 0;
for (let i = 0; i <= max; i++) {
while (count[i]--) {
arr[k++] = i;//多个相同的数
}
}
}
9. 桶排序
桶排序是对计数排序的优化,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况
时间复杂度:O(n+k) k是遍历桶所需时间,n是遍历数组所需时间
空间复杂度:O(n+k) k记录桶,n为桶里的元素
function bucketSort(arr) {
let len = arr.length;
let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER;
/* 遍历数组求总区间 */
for (let i = 0; i < len; i++) {
max = arr[i] > max ? arr[i] : max;
min = arr[i] < min ? arr[i] : min;
}
/* 设置每个桶代表的区间长度 */
let range = Math.ceil((max - min) / len);
/* 将元素放置到对应桶 */
let buckets = [];
for (let i = 0; i < len; i++) {
/* 桶编号 */
let num = Math.floor((arr[i] - min) / range);
if (!buckets[num]) buckets[num] = [arr[i]];
else buckets[num].push(arr[i]);
}
/* 对桶里的元素进行再次排序,然后放到原数组 */
let k = 0;
for (let i = 0; i <= range; i++) {
if (Array.isArray(buckets[i])) {
/* 排序 */
buckets[i].sort((x, y) => x - y);
/* 放置到原数组 */
buckets[i].forEach(v => arr[k++] = v);
}
}
}
10. 基数排序
将整数按位数切割成不同的数字,然后按每个位数分别比较。
时间复杂度:每一位都要遍历一次数组,复杂度为O(n*k),k为最大位数
空间复杂度:桶的个数加上存的元素个数O(n+k)
稳定性:稳定,相同顺序的值进入顺序和出去不变
function getMaxDigit(arr) {
let max = 0;
for (let i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i].toString().length);
}
return max;
}
function radixSort(arr) {
let maxLen = getMaxDigit(arr);//最大位数
/* 根据第i位数字排序 */
for (let i = 1; i <= maxLen; i++) {
let dev = Math.pow(10, i - 1), buckets = [];
/* 遍历数组,根据当前位的值加入到对应桶 */
for (let j = 0; j < arr.length; j++) {
/* 获取第i位数值 */
let num = Math.floor(arr[j] / dev) % 10;
buckets[num] = buckets[num] ? buckets[num] : [];
buckets[num].push(arr[j]);
}
/* 原数组重新赋值 */
arr = [];
for (let j = 0; j < 10; j++) {
if (Array.isArray(buckets[j]))
buckets[j].forEach(v => arr.push(v))
}
}
return arr;
}
/* 测试 */
let arr = [1, 39, 3, 2, 43, 231, 55];
arr = radixSort(arr);
console.log(arr);//[1, 2, 3, 39, 43, 55, 231]