以下为我整理的有关排序的知识,重点是用JS实现的几种常用排序算法,还在不断更新中:
JavaScript中的 sort( ) 方法
sort()方法用于对JS的数组的元素进行排序,该方法会直接改变原始数组。
let a=[40,5,6,23,4]
a.sort()
console.log(a) // [23, 4, 40, 5, 6] ???
- 当数组长度小于等于10的时候,采用插入排序,大于10的时候,采用快速排序。
- 默认排序顺序为按字母升序。如果处理字符串类型的数组直接调用就可以,数值型可能会返回错误。默认数字"23"将排在"4"前面。
let fruits = ["Banana", "Orange", "Apple", "Mango"]; fruits.sort(); // 字母升序 console.log(fruits); // ["Apple", "Banana", "Mango", "Orange"] - 若要使用数字排序,必须在
sort()内传入一个函数作为参数来指定升序/降序。let a= [40,5,6,23,4]; a.sort((a,b)=>{return a-b}); // 数字升序 console.log(a) // [4, 5, 6, 23, 40]
排序算法的评价指标
- 执行时间:主要消耗在比较和移动上,用时间复杂度来衡量,理想情况为
O(logn)。 - 辅助数组:除去待排序数组所占空间之外,需要的额外存储空间,用空间复杂度来衡量,理想情况下为
O(1)。
1 插入排序 InsertSort
排序过程:初始序列只有a[0],从a[1]到a[n-1],按照顺序每次选择一个元素插入之前的序列,使这个序列仍是有序的。
const a=[3,5,2,4,7,1,9,8,6];
元素个数为n,执行n-1趟
初始a[0]不用比较 (3) 5 2 4 7 1 9 8 6 下一个待比较元素 5
i=1 第一趟比较后: (3 5) 2 4 7 1 9 8 6 下一个待比较元素 2
i=2 第二趟比较后: (2 3 5) 4 7 1 9 8 6 下一个待比较元素 4
i=3 第三趟比较后: (2 3 4 5) 7 1 9 8 6 下一个待比较元素 7
i=4 第四趟比较后: (2 3 4 5 7) 1 9 8 6 下一个待比较元素 1
i=5 第五趟比较后: (1 2 3 4 5 7) 9 8 6 下一个待比较元素 9
i=6 第六趟比较后: (1 2 3 4 5 7 9) 8 6 下一个待比较元素 8
i=7 第七趟比较后: (1 2 3 4 5 7 8 9) 6 下一个待比较元素 6
i=8 第八趟比较后: (1 2 3 4 5 6 7 8 9) 全部排序完成
function InsertSort(a,n){
let i,j;
for(i=1;i<a.n;i++){ // a[i] 就是每次选取的比较元素,从a[1]到a[n-1]遍历
let temp=a[i];
for(j=i-1;j>=0;j--){ // a[j] 要从当前 a[i-1] 遍历 a[0],与当前temp进行比较
if(temp<a[j]){ // 如果原排序数组 [1,3,4] temp 为 2 那么要把3,4依次往后挪
a[j+1]=a[j];
}else { //遇到第一个比 temp 小的,就停止比较
break;
}
}
a[j+1]=temp; //把 temp 插入到 a[j+1] 位置
}
return a;
}
const a=[3,5,2,4,7,1,9,8,6];
console.log(InsertSort(a,a.length)); //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
- 时间复杂度:
O(n^2)两层循环,同最坏。外层一定要执行n-1趟,内层要最好情况比较一次,不移动,最差要比较i次,移动i+1次。 - 空间复杂度:
O(1)仅用到一个temp变量存储每轮要比较的a[i] - 排序算法稳定
2 冒泡排序 BubbleSort
排序过程如下:比较a[0]与a[1]大小,若为逆序,则交换两个元素;比较a[1]与a[2]大小,若为逆序,则交换两个元素......这样第一趟后就使得最大的数在在其正确的位置a[n-1]上。
元素个数为n,执行n-1趟
初始状态: 3 5 2 4 7 1 9 8 6
第一趟后: 3 5 2 4 7 1 8 6 (9) 找到了第1大的元素
第二趟后: 3 5 2 4 7 1 6 (8 9) 找到了第2大的元素
第三趟后: 3 5 2 4 1 6 (7 8 9) ...
第四趟后: 3 5 2 4 1 (6 7 8 9)
第五趟后: 3 2 4 1 (5 6 7 8 9)
第六趟后: 3 2 1 (4 5 6 7 8 9)
第七趟后: 2 1 (3 4 5 6 7 8 9)
第八趟后: 1 (2 3 4 5 6 7 8 9) 找到了第8大的元素,结束排序
function BubbleSort(a,n){
for(let i=0;i<n;i++){ //控制比较轮次,一共 n-1 趟
for(let j=i+1;j<n-1-i;j++){
if(a[i]>a[j]){
let temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
}
return a;
}
const a=[3,5,2,4,7,1,9,8,6];
console.log(BubbleSort(a,a.length));
- 时间复杂度:
O(n^2)两层循环,同最坏 - 空间复杂度:
O(1)仅在交换a[i]与a[j]时用到一个temp变量 - 排序算法稳定
3 快速排序 QuickSort
每趟选择一个标兵pivot(通常取该数组段的第一个元素),暂存它的值temp,并设定两个指针i与j,分别指向该数组段的首尾(a[0]与a[n-1]),如果a[i]<a[j],那么就执行i++或者j--(pivot被哪个指针指向,就移动另一个指针),如果a[i]>a[j],那么就swap(a[i],a[j]),并继续移动指针,等到i>j时,停止以上操作。
元素个数为n,执行的趟数取决于递归树的深度。
初始状态: 3 5 2 4 7 1 9 8 6 选取下一趟pivot为3
第一趟后:(1 2) 3 (4 7 5 9 8 6) 标兵3的位置一定正确,对两侧数组选取标兵为1 4
第二趟后: 1 (2) 3 4 (7 5 9 8 6) 标兵1 3 4位置一定正确,左侧数组只剩一个元素,结束比较,右侧选取标兵为7
第三趟后: 1 2 3 4 (6 5) 7 (8 9) 继续选取标兵 6 8
第四趟后: 1 2 3 4 6 (5) 7 8 (9) 所有数组段内的元素都有序,结束比较
每趟选择的标兵pivot会将数组分成两段,对每段数组的排序可以继续选取标兵,然后递归调用该方法去对标兵进行排序,递归的终止条件为 i>j时,就停止排序,此时的数组已经排好序了。
function QuickSort(a,left,right){
if(left>right){ //递归终止条件
return;
}
let i=left,j=right; //设定两个指针i j
let pivot=a[left];
while(i<j){
if(a[i]<=a[j]){ //这一步必须有=
if(a[i]===pivot){
j--;
}else {
i++;
}
}else {
let tmp=a[i];
a[i]=a[j];
a[j]=tmp;
}
}
let pivotIndex; //标兵的index值
for(pivotIndex=left;pivotIndex<=right;pivotIndex++){
if(a[pivotIndex]===pivot){
break;
}
}
QuickSort(a,left,pivotIndex-1); //递归调用pivot左边的数组进行排序
QuickSort(a,pivotIndex+1,right); //递归调用pivot右边的数组进行排序
}
const a=[3,5,2,4,7,1,9,8,6];
QuickSort(a,0,a.length-1); //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
console.log(a);
- 时间复杂度: 平均
O(nlogn),同最好- 最好的情况
O(nlogn):因为每一趟中确定pivotIndex的时间复杂度为O(n),若每一趟后pivot能将左右两段的数据分割成长度大致相同,那么需要 logn 趟就能完成所有数字的排序,递归树如下:
1000 个 数 / \ 500 pivot 500 / \ / \ 250 250 250 250 / \ / \ / \ / \ ... ... ... ... ... ... ...- 最坏的情况
O(n^2):递归树变成单支树,即每次选出的pivot只能划分得到一个比上一次少一个元素的数组段,这样必须经过n-1趟才能完成排序。
1000 个 数 / \ 998 pivot 1 / \ 997 1 / \ ... 1 - 最好的情况
- 空间复杂度:最好
O(logn)最坏O(n)每次递归时,交换a[i]与a[j]需要一个temp变量去暂存数据。 - 排序算法不稳定,因为存在交换 此外,还有一些简便的快排写法:
function QuickSort(a){
if(a.length <= 1){
return a;
}
const left = [],right = [];
const pivotIndex = parseInt(a.length / 2);
const pivot= a[pivotIndex];
for(var i = 0 ; i < a.length ; i++){
if(i===pivotIndex)
continue;
if( a[i] < pivot){
left.push(a[i])
}else{
right.push(a[i]);
}
}
return QuickSort(left).concat([pivot],QuickSort(right));
}
const a=[3,5,2,4,7,1,9,8,6];
console.log(QuickSort(a)); //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
4 选择排序 SelectionSort
- 第一趟从a[0]开始,通过n-1次的比较,选出最小的数a[k],交换a[0]与a[k]。
- 第二趟从a[1]开始,通过n-2次的比较,选出最小的数a[k],交换a[1]与a[k]。
- ...
- 第
n-1趟从a[n-2]开始,通过1次的比较,选出最小的数a[k],交换a[n-2]与a[k]。 元素个数为n,执行n-1趟。
初始状态: 3 5 2 4 7 1 9 8 6
第一趟后:(1) 5 2 4 7 3 9 8 6 交换1与3
第二趟后:(1 2) 5 4 7 3 9 8 6 交换2与5
第三趟后:(1 2 3) 4 7 5 9 8 6 交换3与5
第四趟后:(1 2 3 4) 7 5 9 8 6 比较后无需交换
第五趟后:(1 2 3 4 5) 7 9 8 6 交换5与7
第六趟后:(1 2 3 4 5 6) 9 8 7 交换6与7
第七趟后:(1 2 3 4 5 6 7) 8 9 交换7与9
第八趟后:(1 2 3 4 5 6 7 8) 9 比较后无需交换
function SelectionSort(a,n){
for(let i=0;i<n;i++){ //比较 n-1 趟
let minIndex=i,min=a[i];
for(let k=i+1;k<n;k++){ //k从i+1位遍历到n-1位
if(a[k]<min){
min=a[k];
minIndex=k; //记录最小值的索引
}
}
let temp=a[i]; //交换a[i]与a[minIndex]的值
a[i]=a[minIndex];
a[minIndex]=temp;
}
return a;
}
const a=[3,5,2,4,7,1,9,8,6];
console.log(SelectionSort(a,a.length)); //[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
- 时间复杂度:
O(n^2)两层循环,同最坏。外层一定要执行n-1趟,内层要最好情况每趟都不交换,最差情况每趟都要比较完交换。 - 空间复杂度:
O(1)仅用到temp、minIndex等变量。 - 排序算法不稳定,因为存在交换。
总结
| 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 插入排序 | 平均O(n^2) 最好O(n) 最坏O(n^2) | O(1) | 稳定 | 基本有序,n小 |
| 冒泡排序 | 平均O(n^2) 最好O(n) 最坏O(n^2) | O(1) | 稳定 | 基本有序,n小 |
| 快速排序 | 平均O(nlogn) 最好O(nlogn) 最坏O(n^2) | O(logn) | 不稳定 | 无序,n较大 |
| 选择排序 | 平均O(n^2) 最好O(n) 最坏O(n^2) | O(1) | 不稳定 | 基本有序,n小 |
排序应用
寻找第K大 —— 快速排序 + 递归
利用快速排序的思想寻找数组中的第k大元素,数组内有重复数字。
- 每次设置一个标兵pivot,按照降序排序,将大于pivot的移到左边,小于pivot的移到右边,获得它的索引
pivotIndex。 - 如果每次找到的 pivot 左边刚好有K-1个比它大的,那么该元素就是第K大,即
pivotIndex===k-1 - 如果
pivotIndex>k-1,说明左边的元素比K-1多,说明第K大在其左边,不用管pivot右边,直接递归进入左边。 - 如果
pivotIndex<k-1,说明它左边的元素比K-1少,那第K大在其右边,不用管pivot左边,直接递归进入右边。 - 记得设置终止条件,即
left>right时返回。
function findKth(a,n,k) {
return quickSort(a,0,n-1,k);
}
function quickSort(a,left,right,k){
if(left>right){
return;
}
let pivot=a[left];
let index=left;
let i=left,j=right;
while(i<j){
if(a[i]>=a[j]){ //注意这里
if(a[i]===pivot){
j--;
}else {
i++;
}
}else {
index=(index===i)?j:i; //标兵的index一直在变
let temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
if(index===k-1){
return a[index];
}else if(index>k-1){
return quickSort(a,left,index-1,k);
}else {
return quickSort(a,index+1,right,k);
}
}
let a=[1,3,5,2,2];
console.log(findKth(a,a.length,3)); // 2
还在不断更新中~