排序算法对于大部分程序员来说都不陌生,很多人第一次在编码的过程中遇到的第一个算法可能就是排序算法。而且很多编程语言内部就内置了排序算法,比如 JS
和 Java
中的 sort
。
本文会介绍如何怎么用 JS
写我们常见的几种排序算法(冒泡排序、插入排序、选择排序、归并排序、快速排序)
一些概念
在正片开始前,先说明2个概念
- 原地排序
空间复杂度为O(1)
的排序算法(即除了数据存储本身的空间不需要额外的辅助空间)。 冒泡、插入、选择都是原地排序的算法。 - 算法的稳定性
待排序的序列中值相等的元素,在经过排序算法排序之后,原有的先后顺序保持不变。
冒泡排序
- 时间复杂度:最好
O(n)
, 最差O(n^2)
- 是否原地排序:✅
- 是否稳定排序:✅
- 思路:相邻的元素一一比较
js
/** 冒泡排序 */
const bubbleSort = (arr) => {
let swapped = false; // 是否产生过交换
const length = arr.length;
for (let i = 0; i < length - 1; i++) {
for (let j = 0; j < length - i - 1; j++) {
// 交换
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
}
}
if (!swapped) {
return arr;
}
swapped = false;
}
return arr;
};
插入排序
- 时间复杂度:最好
O(n)
, 最差O(n^2)
- 是否原地排序:✅
- 是否稳定排序:✅
- 思路:将数据分为已排序区间 和未排序区间,取未排序区间的元素在已排序区间中找到合适的位置插入,并且保持已排序区间的数据一直有序
js
const insertSort = (arr) => {
const length = arr.length;
let target;
for(let i = 0 ;i < length; i++) {
target = arr[i];
let j = i - 1;
for(; j >=0 ;j--) {
if (arr[j] > target) {
arr[j + 1] = arr[j] // 移动数据
} else {
break;
}
}
arr[j + 1] = target // 插入数据
}
return arr;
};
选择排序
- 时间复杂度:最好最差都是
O(n^2)
- 是否原地排序:✅
- 是否稳定排序:❌
- 思路:将数据分为已排序区间 和未排序区间,取未排序区间的元素中最小的元素,把它放在已排序区间的末尾
js
const selectSort = (arr) => {
let min;
const length = arr.length;
for (let i = 0; i < length - 1; i++) {
min = i;
for (let j = i + 1; j < length; j++) {
if(arr[j] < arr[min]) {
min = j;
}
}
[arr[min], arr[i]] = [arr[i], arr[min]];
}
return arr;
};
归并排序
- 时间复杂度:
O(nlogn)
- 空间复杂度
O(n)
- 是否原地排序:❌
- 是否稳定排序:✅
- 思路:分治思想,把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。(递归)
- 图解:
js
const merge = (leftArray, rightArray) => {
const mergeArray: number[] = [];
while (leftArray.length && rightArray.length) {
if (leftArray[0] < rightArray[0]) {
mergeArray.push(leftArray.shift());
} else {
mergeArray.push(rightArray.shift());
}
}
return mergeArray.concat(leftArray, rightArray);
}
const mergeSort = (arr) => {
if (arr.length < 2) {
return arr;
}
const mid = Math.floor(arr.length / 2);
// 将数组分成两部分
const leftArr = mergeSort(arr.slice(0, mid));
const rightArr = mergeSort(arr.slice(mid, arr.length));
// 合并左右数组
return merge(leftArr, rightArr);
}
快速排序
- 时间复杂度:
O(nlogn)
- 是否原地排序:✅ (根据实现方式区分)
- 是否稳定排序:❌
- 思路:
- 先从数组中找一个基准数
- 让其他比它大的元素移动到数列一边,比他小的元素移动到数列另一边,从而把数组拆解成两个部分。
- 再对左右区间重复第二步,直到各区间只有一个数
这里根据是否原地排序可以分为两种写法
原地快排
js
quickSort = (arr) => {
// 交换的方法
const swap = (swapArr, a, b) => {
[swapArr[a], swapArr[b]] = [swapArr[b], swapArr[a]];
};
// 分区函数
const partition = (partArray, left, right) => {
const pivot = partArray[right];
let storeIndex = left;
// 交换元素 保证原地快排
for(let i = left; i < right; i++) {
if(partArray[i] < pivot) {
swap(partArray, i, storeIndex);
storeIndex++;
}
}
swap(partArray, right, storeIndex);
return storeIndex;
};
const sort = (sortArray, left, right) => {
if (left > right) {
return;
}
const pivotIndex = partition(sortArray, left, right);
sort(sortArray, left, pivotIndex - 1);
sort(sortArray, pivotIndex + 1, right);
}
sort(arr, 0, arr.length - 1);
return arr;
};
非原地快排
js
const quickSort = (arr) => {
if(arr.length < 2) {
return arr
}
const pivotIndex = Math.floor(arr.length / 2)
const pivot = arr.splice(pivotIndex, 1)[0] // 找到分区点
const leftArray = [], rightArray = []
for(let i = 0; i < arr.length; i++) {
if(arr[i] < pivot) {
leftArray.push(arr[i])
} else {
rightArray.push(arr[i])
}
}
return [...quickSort(leftArray), pivot, ...quickSort(rightArray)]
}