JavaScript提供了多种排序算法,这里整理了六种常用的方法:
Array.prototype.sort():
这是数组对象自带的排序方法,它可以对数组中的元素进行原地排序,默认情况下是按照 Unicode 码位进行升序排列。如果需要按照其他方式排序,可以传入一个比较函数作为参数。
示例:
// 对数字数组进行排序
const arr1 = [7, 3, 9, 2, 10];
arr1.sort((a, b) => a - b); // 升序排序
console.log(arr1); // [2, 3, 7, 9, 10]
// 对字符串数组进行排序
const arr2 = ['cat', 'apple', 'dog', 'banana'];
arr2.sort(); // 默认按 Unicode 编码升序排列
console.log(arr2); // ["apple", "banana", "cat", "dog"]
arr2.sort((a, b) => a.localeCompare(b)); // 按字母顺序升序排列
console.log(arr2); // ["apple", "banana", "cat", "dog"]
// 对对象数组进行排序
const arr3 = [
{ name: 'Tom', age: 18 },
{ name: 'Jack', age: 20 },
{ name: 'Lucy', age: 16 }
];
arr3.sort((a, b) => a.age - b.age); // 按照年龄升序排列
console.log(arr3);
// [
// { name: 'Lucy', age: 16 },
// { name: 'Tom', age: 18 },
// { name: 'Jack', age: 20 }
// ]
需要注意的是,sort() 方法会改变原数组并返回排序后的结果。如果要保留原数组并得到一个新数组,可以使用 slice() 方法创建一个原数组的浅拷贝,然后再对拷贝后的数组进行排序操作。
冒泡排序(Bubble Sort):
这是一种简单但效率较低的排序算法,它的基本思路是从待排序的元素序列的开头开始,依次比较相邻两个元素的大小关系,若前者大于后者则交换它们的位置,并继续往下比较,直到序列末尾为止。重复地进行这个过程,直到整个序列都有序。时间复杂度为 O(n^2)。
具体实现步骤如下:
1、从头开始遍历序列中的每一个元素,将其与相邻的元素比较并进行比较交换,从而让序列中最小的元素“浮”到顶部。
2、对除第一个元素之外的所有元素进行第一步的操作,直到所有元素都排好序为止。
示例:
function bubbleSort(arr) {
const len = arr.length;
for (let i = 0; i < len; i++) {
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]];
}
}
}
return arr;
}
以上代码中,我们使用两层循环来实现冒泡排序。在外层循环中,记录当前待排序序列的长度,并在内层循环中对相邻元素进行比较并交换,这样每次循环都可以将该序列中最小的元素扔到前面。具体地,内层循环的结束条件是
len - i - 1
,这是因为每一轮冒泡排序中已经将排好序的元素移动到了序列末尾,所以在下一轮排序时需要排除掉上一轮已经排序完成的部分。
选择排序(Selection Sort)
选择排序(Selection Sort)是一种简单直观的排序算法,其基本思路是每次从待排序的元素中选取最小(或最大)的一个放到已排序序列的末尾,直到排序完成。时间复杂度为 O(n^2)。
具体实现步骤如下:
-
将序列中的第一个元素作为已排序序列。
-
从未排序序列中找到最小(或最大)的元素,并将其与未排序序列的第一个元素进行交换。
-
将未排序序列的起始位置向后移动一位,再重复第二步操作,直到所有元素都被排序。
function selectionSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i; // 记录最小值的索引位置
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 找到更小的值,更新最小值索引
}
}
// 将最小值与待排序元素第一个位置交换
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
return arr;
}
如上,我们使用两层循环来实现选择排序。在外层循环中,记录当前待排序序列的第一个位置,然后在内层循环中查找到该序列中最小元素的索引位置,再将其与待排序序列第一个元素交换位置。这样每次循环都可以将剩余未排序序列中最小的元素扔到已排序序列的末尾。
快速排序(Quick Sort):
这是一种高效的排序算法,通过分治策略将一个大问题转化成几个小问题,并递归处理,最终将各个子问题的解合并得到最终结果。时间复杂度平均为 O(nlogn),最坏情况下为 O(n^2)。
具体过程如下:
- 首先选取一个基准元素(pivot),通常是选取数组第一个元素或最后一个元素。
- 将数组中大于等于基准值的元素放在基准值右侧,小于基准值的元素放在基准值左侧。
- 对左右两个子数组递归地进行快速排序,直到子数组长度为 1 或 0。
实现过程中的关键是如何将数组分成两部分,并保证左半边数组都小于等于基准元素、右半边数组都大于等于基准元素。可以通过双指针遍历数组的方式实现。具体实现如下:
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivotValue = arr[0]; // 取第一个元素作为基准值
let left = [];
let right = [];
for (let i = 1; i < arr.length; i++) { // 从数组第二个元素开始遍历
if (arr[i] < pivotValue) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
left = quickSort(left); // 左半边数组递归进行快速排序
right = quickSort(right); // 右半边数组递归进行快速排序
return left.concat([pivotValue], right); // 拼接左半边数组、基准值、右半边数组并返回
}
快速排序的时间复杂度为 O(nlogn) 或 O(n^2),具体取决于选取的基准元素以及数组的初始顺序。在最坏情况下(即每次选取的基准元素都是最大或最小值),时间复杂度为 O(n^2),但在平均情况下,时间复杂度为 O(nlogn)。由于快速排序采用了递归的方式,因此它的空间复杂度为 O(logn)。
归并排序(Merge Sort):
这也是一种分治式的比较排序算法,将待排数组不断划分成更小的子数组,直到每个子数组只有一个元素,然后再将相邻的子数组合并成有序的新数组,如此递归下去,最终得到一个完整有序的数组。
具体过程如下:
1.将数组从中间位置分为左右两部分,分别对左右两部分进行归并排序(递归进行直到子数组大小为 1)。
2.对已排序的两个子数组进行合并操作(合并操作即将两个有序数组合并成一个有序数组)。
在实现“合并操作”时,可以新建一个临时数组用于存放已排序的元素,具体实现如下:
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivot = Math.floor(arr.length / 2); // 取数组中间位置
const leftArr = arr.slice(0, pivot); // 分割数组为左右两部分
const rightArr = arr.slice(pivot);
const merge = (left, right) => {
const temp = [];
while (left.length && right.length) {
if (left[0] <= right[0]) { // 从小到大排序
temp.push(left.shift());
} else {
temp.push(right.shift());
}
}
return [...temp, ...left, ...right]; // 拼接剩余未处理的数组并返回
};
return merge(mergeSort(leftArr), mergeSort(rightArr)); // 对左右两部分递归进行归并排序,然后再合并成新的有序数组
}
归并排序的时间复杂度始终为 O(nlogn),但是它需要额外的空间存储临时数组,因此空间复杂度为 O(n)。由于使用了递归的方式,所以对栈空间的开销比较大。
插入排序(Insertion Sort):
这是一种简单而有效的排序算法,是它比较适合对于比较小的数组进行排序。具体思路是将待排元素插入到已排序的合适位置中,从而不断扩大已排序部分,最终得到完整有序的数组。它的思路类似于将一张扑克牌逐个插入到一个已排好序的扑克牌序列中。时间复杂度为 O(n^2),但对于部分有序的数组性能会比其他算法更好。
具体过程如下:
1.将数组中第一个元素看作已经有序的子数组。
2.依次将未排序的元素插入到已排序的合适位置中,直到所有元素都被处理过。
在实现“插入”操作时,可以通过不断比较前一个元素是否大于当前元素,向前移动已排序的元素,并将当前元素插入到合适位置的方式完成。具体实现如下:
function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) { // 从第二个元素开始遍历
let j = i;
while (j > 0 && arr[j] < arr[j - 1]) { // 将当前元素插入到已排序的合适位置中
[arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
j--;
}
}
return arr;
}
插入排序的时间复杂度为 O(n^2),由于它是一种稳定排序算法且只需要常量级别的额外空间,因此在简单应用和小数据集的场景下,插入排序的性能表现较好。
以上是 JavaScript 中常见的排序算法,不同的排序算法在不同的场景下具有不同的优劣,我们需要根据实际需求来选择合适的算法。