这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
立冬快乐!今天你进补了吗? 欢迎回来收看比较排序七剑客下集🤗🤗
4. 归并排序
归并排序采用的是“分治法”的思想,将我们要排序的数组分为前一半和后一半分别进行排序,排序之后得到两个有序的数组再进行合并,采取递归的方式实现。
时间复杂度采用master公式可写成: T(n)=2T(n/2)+O(n),其中n为父过程样本量,n/2为子过程样本量,a是发生的次数,O(n)为合并两个有序数组的额外工作量。所以依据规律,归并排序的时间复杂度为O(n*logn)。
//合并两个有序数组
function merge(arr,l,mid,r){
let help=[];
let i=0;
let p1=l;
let p2=mid+1;
while(p1<=mid&&p2<=r){
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=r){
help[i++]=arr[p2++];
}
//将合并完的有序数组复制回原数组的对应位置
for(i=0;i<help.length;i++){
arr[l+i]=help[i];
}
}
function mergeSort(arr){
if(arr.length<2)return;
subMergeSort=(arr,l,r)=>{
if(l==r)return;
let mid=l+((r-l)>>1);//得出中间值防止溢出的写法
subMergeSort(arr,l,mid);
subMergeSort(arr,mid+1,r);
merge(arr,l,mid,r);
}
subMergeSort(arr,0,arr.length-1);
}
5. 快速排序
经典快排是以数组最后一个数作为基准数,再将比它小的元素放到前面,比它大的数放到后面,然后再分别对基准数前面的元素和基准数后面的元素进行快排,依旧是采用递归的方式实现。
function partition(arr, l, r) {
let base = arr[r];
let i = l;
while (l < r) {
if (arr[l] < base) {
[arr[i], arr[l]] = [arr[l], arr[i]];
i++;
}
l++;
}
[arr[i], arr[r]] = [arr[r], arr[i]];
return i;
}
function quickSort(arr) {
if (arr.length < 2) return;
subQuickSort = (arr, l, r) => {
if (l < r) {
//加入这两句即变为随机快排
//let random=l+Math.floor(Math.random()*(r-l+1));
//[arr[r],arr[random]]=[arr[random],arr[r]];
let base = partition(arr, l, r);
subQuickSort(arr, l, base - 1);
subQuickSort(arr, base + 1, r);
}
}
subQuickSort(arr, 0, arr.length - 1);
}
经典快排存在的问题有在最坏情况下如数组为完全倒序时[5,4,3,2,1],会退化为O(n^2)的时间复杂度,,即和数据状况有关系,所以将其改为随机快排(见代码注释)。 我们还可以利用“荷兰国旗”对代码进行进一步的优化,因为以上代码在每次分区的时候只会确定一个位置(即base基准数),可以改为每次分区时确定一个边界(值等于base基准数的一个边界),让快排变得更快,如下:
function partition(arr, l, r) {
let base = arr[r];
let less=l-1;
let more=r;
while (l < more) {
if(arr[l]<base){
less++;
[arr[less],arr[l]]=[arr[l],arr[less]];
l++;
}else if(arr[l]>base){
more--;
[arr[more],arr[l]]=[arr[l],arr[more]];
}else{
l++;
}
}
[arr[more], arr[r]] = [arr[r], arr[more]];
return [less+1,more];//返回的是一个边界
}
function quickSort(arr) {
if (arr.length < 2) return;
subQuickSort = (arr, l, r) => {
if (l < r) {
let random=l+Math.floor(Math.random()*(r-l+1));
[arr[r],arr[random]]=[arr[random],arr[r]];
let base = partition(arr, l, r);
console.log(base);
subQuickSort(arr, l, base[0] - 1);
subQuickSort(arr, base[1] + 1, r);
}
}
subQuickSort(arr, 0, arr.length - 1);
}
6.希尔排序
希尔排序是在插入排序的基础上进行改进的,数组本身的有序性会影响插入排序的效率,那么我们可以在进行插入排序之前先进行数组有序性的调整。 如[10,13,1,3,9,8,6,7],采用传统的希尔增量为步长,首先是数组长度的一半4,所以10和9,13和8,1和6一组,3和7一组,经过调整之后10和9调换位置,13和8调换位置,1和6位置不变,3和变为[9,8,1,3,10,13,6,7]。然后我们再把跨度缩小为上一次的一半,变成2,所以9,1,10,6一组,8,3,13,7一组,调整之后变为1,6,9,10和3,7,8,13,数组变为[1,3,6,7,9,8,10,13]。有没有发现数组经过这样一番“折腾”之后变得有序多了咧~跨度再次缩小为上一次的一半,就变成1了,也就是我们上面说过的插入排序啦。
function shellSort(arr){
if(arr.length<2)return;
let step=Math.floor(arr.length/2);//初始步长为数组长度的一半
while(step>=1){
for(let i=step;i<arr.length;i++){
let temp=arr[i];
let j=i;
while(arr[j-step]>temp&&j-step>=0){
arr[j]=arr[j-step];
j-=step;
}
arr[j]=temp;
}
step=Math.floor(step/2);//缩小步长
}
}
但是采用传统的希尔增量,会存在一个问题,我们看看接下来这个数组[2,1,5,3,7,6,9,8],当我们继续用4,2作为增量的话,会发现元素根本不会有任何交换,直至我们把增量变为1的时候才会进行调整,那不就是相当于在选择排序之前还白白浪费了以4和以2作为增量的元素之间的比较吗!所以为了保证插入排序前的调整不会白费,人们又提出了更好的增量序列:Hibbard增量序列和Sedgewick增量序列。等俺整明白了再来更新一波~
7.堆排序
堆,其实就是一棵完全二叉树。所以一个节点,记为i的话,它的左孩子就是2i+1,右孩子就是2i+2,父节点就是(i-1)/2。 我们还需要了解两个操作,一个是heapInsert,一个是heapify。heapInsert就是向堆中插入元素,插入时与父节点进行迭代比较,比父节点大就进行交换,直到不比父节点大或者到达顶端。heapify是解决当堆中某个元素发生变化时如何重新调整为堆结构的问题。 而堆排序是如何进行的呢?以大根堆为例,当将所有元素通过heapInsert操作之后,就构成了一个大根堆,之后我再把堆顶元素和最后一个元素调换位置,这样最大的元素就放到了数组的最后。而因为我们将最后一个元素放到了堆顶,破坏了原来的大顶堆,所以在0-(N-2)之间需要进行堆的调整(此时N-1的位置已经是最大的元素啦)。然后继续重复将堆顶元素和本次最后一个位置元素进行调换,再进行堆调整的操作,直至我们的数组排好序为止。
function heapInsert(arr,index){
while(arr[index]>arr[Math.floor((index-1)/2)]){//与父节点的值进行比较
[arr[index],arr[Math.floor((index-1)/2)]]=[arr[Math.floor((index-1)/2)],arr[index]];
index=Math.floor((index-1)/2);
}
}
function heapify(arr,index,size){
let left=index*2+1;
while(left<size){
let largest=left+1<size&&arr[left+1]>arr[left]?left+1:left;//left+1即是右孩子
largest=arr[largest]>arr[index]?largest:index;
if(largest==index)break;//如果比左右孩子的值都大就说明堆调整已经ok啦
[arr[largest],arr[index]]=[arr[index],arr[largest]];
index=largest;
left=index*2+1;
}
}
function heapSort(arr){
if(arr.length<2)return;
for(let i=0;i<arr.length;i++){
heapInsert(arr,i);
}
let size=arr.length;
size--;
[arr[0],arr[size]]=[arr[size],arr[0]];//将堆顶元素和最后一个元素调换位置
while(size>0){
heapify(arr,0,size);
size--;
[arr[0],arr[size]]=[arr[size],arr[0]];
}
}
以上权当过去这一周复习了七种排序算法的学习记录啦(从“堆排序是个啥,根本不想懂”到“噢,原来这就是堆排序啊”),感谢左神(yyds),让我重新燃起对算法的小小热情,新的一周,好好学习🤤🤤