在JavaScript开发中,排序是一项基础而重要的操作。本文将探讨JavaScript中几种常见的排序算法,包括它们的原理、实现方式以及适用场景。
1、冒泡排序
1.1、原理
通过比较相邻两个数的大小,交换位置排序:如果后一个数比前一个数小,则交换两个数的位置,重复这个过程,直到所有的数据按照升序排列。
1.2、代码实现
采用两层嵌套循环的方案,外层循环控制比较的轮数,内层循环用于比较相邻数据和交换位置
let Arr = [2, 5, 3, 7, 9, 1, 0, 6, 23, 12]
/**
* 冒泡排序:
* 时间复杂度O(n^2)
* 原理:比较相邻的元素,如果第一个比第二个大,就交换它们两个
* 注意:冒泡排序是原地排序算法,会改变原数组中元素顺序
* @param {*} arr
* @returns
*/
const bubbleSort = (arr) => {
let len = arr.length;
if (len <= 1) return arr;
for (let i = 0; i < len; i++) {
// 提前退出冒泡循环的标志位
let swappend = false;
for (let j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swappend = true;
}
}
// 没有数据交换,提前退出
if (!swappend) {
break;
}
}
return arr;
}
// 测试用例
console.log(bubbleSort(Arr));
// [0, 1, 2, 3, 5, 6, 7, 9, 12, 23]
2、快速排序
2.1、原理
采用二分法进行分区,再结果递归进行排序,具体操作如下:
- 选择基准元素(Pivot):从数组中随机选择一个元素作为基准值;
- 基于基准元素进行分区:比基准元素小的放到左边分区,比基准元素大的放到右侧分区;
- 对分区进行递归操作
2.2、代码实现
/**
* 快速排序:
* 时间复杂度O(nlogn)
* 原理:采用二分法,取出中间的元素(基准-pivot),让小于基准的放到左边,大于基准的放到右边进行分区;然后递归的对左右两个分区再次进行二分法操作,进行快速排序
* @param {*} arr
* @returns
*/
const quickSort = (arr) => {
if (arr.length <= 1) {
return arr;
}
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr.splice(pivotIndex, 1)[0];
let left = [];
let right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
}
3、选择排序
3.1、原理
选择排序的原理很简单,它分为以下几个步骤:
- 查找最小(或最大)元素:在未排序的序列中找到最小(或最大)的元素。
- 交换位置:将找到的最小(或最大)元素和序列的第一个元素交换位置,如果最小元素就是序列的第一个元素,则位置保持不变。
- 重复步骤:对剩余未排序的序列重复上述两个步骤。
通过n-1次上述操作,就能完成整个序列的排序。具体来说,第一次从n个数据中选出最小的,放在第一个位置;第二次从剩下的n-1个数据中选出最小的,放在第二个位置;以此类推,直到整个序列变成有序序列。
3.2、代码实现
/**
* 选择排序
* 时间复杂度:O(n^2)
* 原理:每次找到最小值,放到已排序数组的末尾;然后继续从剩余未排序数组中找到最小值,放到已排序数组的末尾
* 说明:选择排序是原地排序算法,会改变原数组中元素顺序
* @param {*} arr
* @returns
*/
const selectionSort = (arr) => {
const len = arr.length;
if (len <= 1) {
return arr
}
for (let i = 0; i < arr.length; i++) {
let minIndex = i;
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex !== i) {
const temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
4、归并排序
4.1、原理
归并排序的原理基于分治法(Divide and Conquer)策略,它包括以下三个步骤:
- 分解:将待排序的n个元素分成各含n/2个元素的子序列。
- 解决:使用归并排序递归地排序两个子序列。
- 合并:合并两个已排序的子序列以产生已排序的答案。
具体来说,归并排序的工作流程如下:
- 递归地分解:从中间将数组分成两半,直到每个子数组只包含一个元素,因为单个元素被认为是已排序的。
- 合并过程:然后开始合并这些子数组,以创建排序好的数组。这个过程是通过比较每个子数组的最前面的元素来完成的,选择两者中较小的那个放入新数组中,并移动所选元素所在数组的指针(或索引),直到所有子数组的元素都被合并成一个完整的排序数组。
4.2、代码实现
/**
* 归并排序:
* 时间复杂度:O(nlogn)
* 原理:采用分治法,将数组分为左右两个数组,然后递归的对左右两个数组进行归并排序,最后合并两个有序数组
* 说明:归并排序是稳定,不会改变原有数组
* 详细步骤:
* 1、递归的将数组分为左右两个数组,直到数组长度为1
* 2、合并两个有序数组
* @param {*} arr
* @returns
*/
const mergeSort = (arr) => {
if (arr.length <= 1) {
return arr;
}
const middle = Math.floor(arr.length / 2);
const left = arr.slice(0, middle);
const right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
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));
}
5、上述排序算法的优缺点
5.1. 冒泡排序
- 时间复杂度:平均和最坏情况为O(n^2),最好情况为O(n)(已经排序的情况)。
- 空间复杂度:O(1),是原地排序算法。
- 特点:实现简单,但效率较低,适用于小数据集的排序。
- 工作原理:通过重复遍历待排序数组,比较相邻元素,若顺序错误则交换,直到整个数组排序完成。
5.2. 快速排序
- 时间复杂度:平均和最好情况为O(n log n),最坏情况为O(n^2)(但可以通过选择合适的枢轴改进)。
- 空间复杂度:O(log n),因为递归调用的栈空间。
- 特点:效率高,是最常用的排序算法之一,但最坏情况下效率较低。
- 工作原理:选择一个元素作为基准,将数组分为两部分,左边都比基准小,右边都比基准大,然后对这两部分递归地进行快速排序。
5.3. 选择排序
- 时间复杂度:平均、最好和最坏情况都为O(n^2)。
- 空间复杂度:O(1),是原地排序算法。
- 特点:实现简单,但效率较低,不适合大数据集。
- 工作原理:遍历数组,每次从未排序的部分选出最小(或最大)的元素,放到已排序部分的末尾。
5.4. 归并排序
- 时间复杂度:平均、最好和最坏情况都为O(n log n)。
- 空间复杂度:O(n),需要额外的存储空间。
- 特点:效率高,稳定排序,适用于大数据集,但需要额外的内存空间。
- 工作原理:将数组分成两半,对每一半递归地应用归并排序,然后将排序好的两半合并成一个有序数组。
总结
- 对于小数据集,冒泡排序和选择排序简单但效率不高。
- 快速排序在大多数情况下提供了很好的性能,是通用的排序算法之一。
- 归并排序提供稳定的排序性能,特别适合于大数据集,但需要额外的内存空间。