排序分类
- 比较排序
这类排序算法通过比较元素之间的大小来决定它们的相对顺序。
常见的比较排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序等。
- 非比较排序
与比较排序相反,非比较排序算法不通过比较元素的大小来确定它们的顺序,而是利用元素本身的特性来进行排序。
典型的非比较排序算法有计数排序、桶排序和基数排序。
- 稳定排序和不稳定排序
稳定排序算法保持相等元素之间的相对顺序不变,而不稳定排序算法则可能改变相等元素的相对顺序。
例如,冒泡排序和插入排序是稳定的,而快速排序是不稳定的。
一、冒泡排序
实现思路:通过重复遍历要排序的列表,依次比较相邻的元素,如果顺序不对则交换它们。
- 简单但效率较低,时间复杂度为 O(n^2)。
- 稳定排序算法。
- 原地排序,空间复杂度为 O(1)。
方法一:基本逻辑实现
相邻元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置,当一个元素小于或等于右侧相邻元素时,位置不变。
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 利用数组解构赋值 交换j和j+1
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
}
方法二:有序标记实现
这是对方法一的优化版,增加有序标记,如果某一轮比较,一个元素都没有交换就代表整个数组已经有序。
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
// 有序标记,每一轮的初始值都是true
let isSorted = true;
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 利用数组解构赋值 交换j和j+1
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
// 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false
isSorted = false;
}
}
if (isSorted) {
break;
}
}
}
方法三:添加边界实现
这是对方法二的进一步优化,优化每一轮比较的次数。增加一个border代表无序边界,然后把最后一次发生元素交换的位置点记下来,下一轮比较就只需要比到这儿为止。
function bubbleSort(arr) {
// 记录最后一次交换的边界
let lastExchangeIndex = 0;
// 无序数组的边界,每次比较只需要比到这里为止
let sortBorder = arr.length - 1;
for (let i = 0; i < arr.length - 1; i++) {
// 有序标记,每一轮的初始值都是true
let isSorted = true;
for (let j = 0; j < sortBorder; j++) {
if (arr[j] > arr[j + 1]) {
// 利用数组解构赋值 交换j和j+1
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
// 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false
isSorted = false;
// 更新为最后一次交换元素的位置
lastExchangeIndex = j;
}
}
sortBorder = lastExchangeIndex;
if (isSorted) {
break;
}
}
}
方法四:鸡尾酒排序实现
鸡尾酒排序算法是改进的冒泡排序,鸡尾酒是双向排序。就像排钟从左摆到右后,立即从当前位置摆回去。
function bubbleSort(arr) {
for (let i = 0; i < arr.length / 2; i++) {
// 有序标记,每一轮的初始值都是true
let isSorted = true;
for (let j = i; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 利用数组解构赋值 交换j和j+1
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
// 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false
isSorted = false;
}
}
if (isSorted) {
break;
}
// 顺摆轮结束之前,把isSorted重新标记为true,下面就开始逆摆
isSorted = true;
for (let j = arr.length - i - 1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
// 利用数组解构赋值 交换j和j+1
[arr[j], arr[j - 1]] = [arr[j - 1], arr[j]]
// 进这个if循环,就代表有元素进行交换,所以就不是有序的,把标记改为false
isSorted = false;
}
}
if (isSorted) {
break;
}
}
}
二、选择排序
实现思路
找到数组中的最小(大)元素,将其放到已排序序列的末尾,然后从剩余未排序元素中继续寻找最小(大)元素。
- 简单但效率较低,时间复杂度为 O(n^2)。
- 不稳定排序算法。
- 原地排序,空间复杂度为 O(1)。
方法一:
function selectionSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i; // minIndex用来记录最小值的下标
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 当循环结束,i和minIndex不相等就说明有最小元素,需要交换
if (i !== minIndex) {
arr[i, minIndex] = arr[minIndex, i]
}
}
}
三、插入排序
实现思路:
将数据分为有序区和无序区,每次从无序区中取出一个元素,然后和有序区从后往前挨个比较,比该元素大的往后移。直到找到比该元素小的元素。那么这个元素就插入到他的后面就行。
- 对于小型数据集或部分有序数据表现良好,平均时间复杂度为 O(n^2)。
- 稳定排序算法。
- 原地排序,空间复杂度为 O(1)。
方法一:
function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j > 0 && arr[j - 1] > arr[j]; j--) {
[arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]; // 利用数组解构赋值交换元素
}
}
return arr;
}
四、快速排序
实现思路
通过选定一个基准值,将数组分为两部分,左边的元素小于基准值,右边的元素大于基准值,然后递归地对左右子数组进行排序。
- 高效的排序算法,平均时间复杂度为 O(n log n)。
- 不稳定排序算法。
- 原地排序,空间复杂度为 O(log n)。
裂缝为观测对象(Hoare)把数组分成两部分:小堆,大堆
需要格外空间的快速排序算法
function crackTheBeastQuickSort(arr) {
if (arr.length <= 1) {
return arr;
}
// 选择随机基准元素
const pivot = arr[Math.floor(Math.random() * arr.length)];
const less = [];
const equal = [];
const greater = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
less.push(arr[i]);
} else if (arr[i] === pivot) {
equal.push(arr[i]);
} else {
greater.push(arr[i]);
}
}
return [...crackTheBeastQuickSort(less), ...equal, ...crackTheBeastQuickSort(greater)];
}
原地排序算法
function partition(arr, low, high) {
const pivot = arr[high];
let i = low - 1;
for (let j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}
function quickSortInPlace(arr, low, high) {
if (low < high) {
const partitionIndex = partition(arr, low, high);
quickSortInPlace(arr, low, partitionIndex - 1);
quickSortInPlace(arr, partitionIndex + 1, high);
}
return arr;
}
五、归并排序
实现思路
将数组分成两个子数组,分别排序,然后合并两个已排序的子数组。
- 稳定的排序算法,时间复杂度为 O(n log n)。
- 需要额外的空间来存储中间结果,空间复杂度为 O(n)。
方法一:
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
const merge = (left, right) => {
let result = [];
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] < right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
};
return merge(mergeSort(left), mergeSort(right));
}
六、堆排序
实现思路
在堆排序中,首先将无序数组构建成最大堆,然后循环删除堆顶元素,将其移到末尾,并调整产生新的堆顶,重复这个过程直到整个数组有序。
- 高效的排序算法,时间复杂度为 O(n log n)。
- 不稳定排序算法。
- 原地排序,空间复杂度为 O(1)。
方法一:
function heapSort(arr) {
// 把无序数组构建成最大堆
for (let i = (arr.length - 2) / 2; i >= 0; i--) {
downAdjust(arr, i, arr.length);
}
// 循环删除堆顶元素,移到末尾,调整产生新的堆顶
for (let i = arr.length - 1; i > 0; i--) {
[arr[i], arr[0]] = [arr[0], arr[i]];
downAdjust(arr, 0, i);
}
return arr;
}
/**
*
* @param {待调整的堆} arr
* @param {要下沉的父节点} parentIndex
* @param {堆的有效大小} length
*/
function downAdjust(arr, parentIndex, length) {
// temp保存父节点值,用户最后的赋值
let temp = arr[parentIndex];
let childIndex = 2 * parentIndex + 1;
while (childIndex < length) {
// 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
childIndex++;
}
// 如果父节点大于任何一个孩子的值,则直接跳出
if (temp >= arr[childIndex]) {
break;
}
// 无须真正交换,单向赋值即可
arr[parentIndex] = arr[childIndex];
parentIndex = childIndex;
childIndex = 2 * childIndex + 1;
}
arr[parentIndex] = temp;
}
七、计数排序
实现思路
统计数组中每个元素出现的次数,根据元素的计数信息将元素放回正确的位置。
- 非比较排序算法,适用于特定范围的整数排序,时间复杂度为 O(n + k)(k 为整数范围)。
- 稳定排序算法。
- 需要额外空间,空间复杂度取决于整数范围。
方法一:
function heapSort(arr) {
// 把无序数组构建成最大堆
for (let i = (arr.length - 2) / 2; i >= 0; i--) {
downAdjust(arr, i, arr.length);
}
// 循环删除堆顶元素,移到末尾,调整产生新的堆顶
for (let i = arr.length - 1; i > 0; i--) {
[arr[i], arr[0]] = [arr[0], arr[i]];
downAdjust(arr, 0, i);
}
return arr;
}
/**
*
* @param {待调整的堆} arr
* @param {要下沉的父节点} parentIndex
* @param {堆的有效大小} length
*/
function downAdjust(arr, parentIndex, length) {
// temp保存父节点值,用户最后的赋值
let temp = arr[parentIndex];
let childIndex = 2 * parentIndex + 1;
while (childIndex < length) {
// 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
childIndex++;
}
// 如果父节点大于任何一个孩子的值,则直接跳出
if (temp >= arr[childIndex]) {
break;
}
// 无须真正交换,单向赋值即可
arr[parentIndex] = arr[childIndex];
parentIndex = childIndex;
childIndex = 2 * childIndex + 1;
}
arr[parentIndex] = temp;
}