Array.prototype.sort()方法
定义
sort() 方法按照某种规则就地对数组的元素进行排序,并返回对相同数组的引用。(不会产生新数组,但是会更改原数组)
默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序。
由于它取决于具体实现,因此无法保证排序的时间和空间复杂度。
如果想要不改变原数组的排序方法,可以使用 toSorted()。
参数
- compareFn(可选):定义排序顺序的函数。返回值应该是一个数字,其正负性表示两个元素的相对顺序。该函数使用以下参数调用
- curItem:当前元素(从第1个元素开始)
- preItem:上一个元素(从第0个元素开始)
如果省略该函数,数组元素会被转换为字符串,然后根据每个字符的 Unicode 码位值进行排序。
compareFn(curItem, preItem) 返回值 | 排序顺序 |
---|---|
> 0 | 不替换顺序 |
< 0 | 替换顺序 [preItem, curItem] = [curItem, preItem] |
=== 0 | 不替换顺序 |
var arr = [2, 3, 1];
arr.sort((cur, pre) => {
console.log(cur, pre);
return cur - pre;
});
console.log(arr); // Array [1, 2, 3]
/*
3 2
1 3
1 3
1 2
*/
返回值
排序后的数组。
注意数组是就地排序的,不会进行复制。
稀疏数组的处理
sort() 方法保留空槽。如果原数组是稀疏的,则空槽会被移动到数组的末尾,并始终排在所有 undefined 元素的后面。
var arr = [2, 3, , 1];
arr.sort((cur, pre) => {
console.log(cur, pre);
return cur - pre;
});
console.log(arr); // Array [1, 2, 3, <1 Empty>]
/*
3 2
1 3
1 3
1 2
*/
如何保证正确的排序行为
为了确保正确的排序行为,比较函数应具有以下属性:
- 纯函数:比较函数不会改变被比较的对象或任何外部状态。(这很重要,因为无法保证比较函数将在何时以及如何调用,因此任何特定的调用都不应对外部产生可见的效果。)
- 稳定性:比较函数对于相同的输入对应始终返回相同的结果。
- 自反性:
compareFn(a, a) === 0
。 - 反对称性:
compareFn(a, b)
和compareFn(b, a)
必须都是 0 或者具有相反的符号。 - 传递性:如果
compareFn(a, b)
和compareFn(b, c)
都是正数、零或负数,则compareFn(a, c)
的符号与前面两个相同。
sort 底层原理
sort 底层采用冒泡排序,时间复杂度为 O(n2)。
冒泡排序的原理:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
function sort(arr, fn) {
for(let i = 0; i < arr.length - 1; i++) {
for(let j = 0; j < arr.length - 1 - i; j++) {
if(fn(arr[j], arr[j + 1]) > 0) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
};
V8中的sort
V8 引擎 sort 函数只给出了两种排序 InsertionSort 和 QuickSort,数组长度小于等于 22 的用插入排序 InsertionSort,比22大的数组则使用快速排序 QuickSort。
插入排序InsertionSort
插入排序是指在待排序的元素中,假设前面 n-1(其中 n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序。
时间复杂度为 O(n2),是稳定的排序。
插入排序的原理:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
/*
实现方式:迭代
arr:原数组
*/
function Insertion(arr) {
// preIndex:已经排好序的数组的最后一个索引
// current:当前要插入的数字
let preIndex, current;
// 循环,从第二个数开始插入
for (let i = 1; i < arr.length; i++) {
preIndex = i - 1;
current = arr[i];
// 循环,将当前要插入的数字与已经排好序的数组从后往前进行比较
while (preIndex >= 0 && current < arr[preIndex]) {
// 数组元素右移
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
// 找到插入位置
arr[preIndex + 1] = current;
}
return arr;
}
快速排序QuickSort
快速排序是对冒泡排序的一种改进。
快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
时间复杂度为 O(n2),是不稳定的排序。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
- 首先设定一个分界值,通过该分界值将数组分成左右两部分
- 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值
- 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理
- 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了
概括来说为 挖坑填数 + 分治法。
/*
实现方式:递归
arr:原数组
start:左边界
end:右边界
*/
function qsort(arr, start, end) {
// pivot:分界值
const pivot = arr[start];
// left:左边界
let left = start;
// right:右边界
let right = end;
while (left < right) {
// 遍历右边界,直到找到小于分界值的值
while (left < right && arr[right] > pivot) right--;
// 遍历左边界,直到找到大于分界值的值
while (left < right && arr[left] < pivot) left++;
// 判断两个值是否相对,不相等则互换
if (arr[left] === arr[right] && left < right) left++;
else {
[arr[left], arr[right]] = [arr[right], arr[left]];
}
}
// 将原数组分为左、右两个数组,进一步递归
if (left - 1 > start) arr = qsort(arr, start, left - 1);
if (right + 1 < end) arr = qsort(arr, right + 1, end);
return arr;
}