归并排序
利用master公式可求得时间复杂度为O(N*logN),额外空间复杂度为O(N)
master公式的具体使用可以去看往期的文章。
/**归并排序
* master:T(N) = 2 * (N/2) + O(N^1)
* log(2,2) = 1
* 所以时间复杂度为O(N^logN)
*/
process1(arr:number[],l:number,r:number,order:Function){
if(l == r) return;
let mid = l + ((r-l)>>1); //防溢出的求中点方式,万一数组很大的情况下
this.process1(arr,l,mid,order);
this.process1(arr,mid + 1,r,order);
this.merge(arr,l,mid,r,order)
}
merge(arr:number[],l:number,m:number,r:number,order:Function){
let help:number[] = [];
let p1 = l;
let p2 = m + 1;
while(p1 <= m && p2 <= r){
if(order(arr[p1],arr[p2])){
help.push(arr[p1++]);
}else{
help.push(arr[p2++]);
}
}
while(p1 <= m){
help.push(arr[p1++]);
}
while(p2 <= r){
help.push(arr[p2++])
}
for(let i = 0; i<help.length;i++){
arr[l+i] = help[i];
}
}
拓展:求数组的中所有数的小和(小和指左边所有比自己小的数之和)
方法:使用归排升序求小和
/**
* 逆向思维:右边有几个数比自己大,就加多少个自己
* 使用归排升序求小和,实现O(N^logN)的时间复杂度
*/
smallSum(arr:number[],l:number,r:number):number{
if(l == r) return 0;
let mid = l + ((r-l)>>1);
let sum1 = this.smallSum(arr,l,mid);
let sum2 = this.smallSum(arr,mid+1,r);
return this.mergeAndS(arr,l,mid,r,sum1 + sum2)
}
mergeAndS(arr:number[],l:number,m:number,r:number,s:number):number{
let help:number[] = [];
let p1 = l;
let p2 = m + 1;
while(p1 <= m && p2 <= r){
if(arr[p1] < arr[p2]){
s+=arr[p1] * (r-p2 + 1);
help.push(arr[p1++]);
}else{
help.push(arr[p2++]);
}
}
while(p1 <= m){
help.push(arr[p1++]);
}
while(p2 <= r){
help.push(arr[p2++])
}
for(let i = 0;i<help.length;i++){
arr[l+i] = help[i];
}
return s;
}
快速排序
快速排序3.0,即每次选择数组中任意的一个数放至队尾,作为划分对象num值进行partition。
时间复杂度,最好的情况为O(NlogN),最坏情况为O(N^2),最后用长期期望算得O(NlogN)
空间复杂度 O(logN),即最坏情况的二叉树高度
/*
*快速排序3.0,即每次选择数组中任意的一个数放至队尾,作为划分对象num值进行partition。
* 时间复杂度,最好的情况为O(N*logN),最坏情况为O(N^2),最后用长期期望算得O(N*logN)
* 空间复杂度为O(logN),即最坏情况的二叉树高度
*/
quickSort(arr:number[],l:number,r:number){
if(l < r){
let ran1 = Math.floor(Math.random() * (r - l + 1));
let ran = l + ran1;
this.swap(arr,ran,r);
let part = this.partition(arr,l,r,arr[r]);
this.quickSort(arr,l,part[0]);
this.quickSort(arr,part[1],r);
}
}
/**
* 荷兰国旗问题(区域划分),设定num值,数组中小于num值的都放在左边,等于的放中间,大于放右边
* 单独执行的空间复杂度O(1),时间复杂度O(n)
*/
partition(arr:number[],l:number,r:number,num:number){
let small: number = l - 1;
let big: number = r + 1;
while (l != big) { //当指针p没有触碰大于区域时则继续
if (arr[l] < num) {
this.swap(arr, l++, ++small)
} else if (arr[l] == num) {
l++;
} else {
this.swap(arr, l, --big);
}
}
return [small,big];
}
堆排序
时间复杂度O(N*logN),空间复杂度O(1)
/**
* 0
* / \
* 1 2
* / \
* 3 4
*(小根堆的定义,分别对应数组中的下标)
/
/**
* 5(0)
* / \
* 4(1) 3(2)
* / \
* 2(3) 1(4)
* (大根堆的定义,括号内分别对应数组中的下标)
*/
/**
* 以小根堆为例,当数据在现在的位置上比父节点小时,与父节点交换位置,直至比父节点大或已处于根节点
* @index 数据当前所在的数组下标位置
*/
heapInsert(arr: number[], index: number, comparator: Function) {
while (index > 0 && comparator(arr[index], arr[Math.floor((index - 1) / 2)])) {
this.swap(arr, Math.floor((index - 1) / 2), index);
index = Math.floor((index - 1) / 2);
}
}
/**
* 以小根堆为例,当数据在现在的位置上比左右子节点中任意一个大时,与该子节点交换位置,直至比左右子节点都小或没有左右子节点
* @heapSize 堆的大小
* @index 数据当前所在的数组下标位置
*/
heapify(arr: number[], index: number, heapSize: number, comparator: Function) {
while ((index * 2 + 1) < heapSize) {
let miner = comparator(arr[index * 2 + 1], arr[index * 2 + 2]) || index * 2 + 2 >= heapSize ? index * 2 + 1 : index * 2 + 2;
if (comparator(arr[index], arr[miner])) break;
else {
this.swap(arr, index, miner);
index = miner;
}
}
}
/**
* 堆排序,将数组数据放入堆结构中,利用堆结构进行排序
* 升序用大根堆,降序用小根堆
* 时间复杂度为O(NlogN),空间复杂度为O(1)
*/
heapSort(arr: number[], comparator: Function) {
if (arr.length < 2) return;
let heapSize = 0;
//普通建堆
// for(let i = 0;i<arr.length;i++){ //O(N)
// this.heapInsert(arr,i,comparator); //O(logN)
// heapSize++;
// }
for (let i = arr.length - 1; i >= 0; i--) { //稍微更快的建堆方法
this.heapify(arr, i, arr.length, comparator);
heapSize++;
}
this.swap(arr, 0, --heapSize)
//排序
while (heapSize > 0) { //O(N)
this.heapify(arr, 0, heapSize, comparator); //O(logN)
this.swap(arr, 0, --heapSize);
}
}
拓展:已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过K,并且K相对于数组来说比较小,请针对这个数据进行排序。
heapSortInK(arr: number[], k: number, comparator: Function) {
//首先定义一个heapSize为k的堆,将数组中的前k个数加进去
let heap = [];
let index = 0
for (let i = 0; i < k; i++) {
heap[i] = arr[i];
this.heapInsert(heap, i, comparator);
}
//然后将后续的数加入到堆中,然后弹出堆顶
for (let i = k; i < arr.length; i++) {
heap[i] = arr[i];
arr[index++] = heap[0];
this.swap(heap, i, 0);
this.heapify(heap, 0, k, comparator);
}
for (let i = k - 1; i >= 0; i--) {
arr[index++] = heap[0];
this.swap(heap, i, 0)
this.heapify(heap, 0, i, comparator)
}
}
基于比较的排序算法的 归纳总结
稳定性 是指相同数据之间在排序后是否能维持原来的先后,
冒泡排序和插入排序中相等不互换,因此稳定
归并排序先录入左边的数字也是稳定的(求小和算法中的归并排序不稳定,因为必须先录入右边的数字)
快速排序与选择一样会交换数字的位置,因此不稳定
堆排序 父子间的比较会互换位置,因此不稳定
时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|
选择排序 | O(N^2) | O(1) | × |
冒泡排序 | O(N^2) | O(1) | √ |
插入排序 | O(N^2) | O(1) | √ |
归并排序 | O(N*logN) | O(N) | √ |
快速排序 | O(N*logN) | O(logN) | × |
堆排序 | O(N*logN) | O(1) | × |