周内要工作,周末要出去玩,留给我学习的时间越来越少了(开始懒惰.jpg)。排序差不多学完了,前面学的差不多忘完了,很好。。。所以我打算总结完之后先练几遍各种排序,手写一遍,练到说到哪个排序立马就能知道思路是什么,脑子里大概浮现出一个代码,然后知道各个排序的优缺点,什么时候用,搞明白搞熟悉之后再学习后面的链表,小笨蛋就是要一步一个脚印慢慢走,工作忙起来下次更布吉岛啥时候了,想起我公众号断更俩年了哈哈哈哈哈,坚持六个月的习惯,只需要一分钟就可以放弃,人总是在和另一个想要舒适的自己作斗争,时而输,时而赢。
一 堆排序(时间复杂度为 O(N * LogN) )
1.什么是大根堆
将一个数组写成二叉树,如果每一个父比所有的子都大,那么这就是二叉树 例如[6,4,5,3,2,1]
graph TD
6 --> 4 -->3
4 -->2
6 --> 5 -->1
堆长度 heapSize = 6
子找父 (i-1)/2
父找子 2i+1
如果子位置>=heapSize,则后面不再有子了
2.大根堆排序思路
第一种,如果heapSize=0,给一个数,进行大根堆排序。思路就是弑父替位,更有歹毒的网友将它称为吕布算法。。。也就是说找到i位置的这个数的父,然后进行比较,如果大于父,则父子交换,i来到了父位,再进行比较,直到小于父或者没有父为止;
第二种 找出已有大根堆的最大值,删除它并且使得剩下的数仍然是大根堆。思路就是,大根堆最大值一定是0位置,将数组最后一个位置的数和0位置的数交换,然后heapSize--,最后一个数来到了0位置后,找到他的儿子们,在儿子中选出最大的儿子进行比较,如果小于儿子,则进行交换,来到了子的位置,然后继续找,直到没有儿子,或者大于儿子为止。
3.案例
//不断向上和父位比较
function heapInsert(arr,index){
while(arr[index]>arr[(index-1)/2]){
swap(arr,index,(index-1)/2); // 交换函数
index=index-1)/2;
}
}
// 不断向下和子位比较
function heapify(arr,index,heapSize){
// 左儿子
let left=2*index+1;
while(left<heapSize){
// 在有右儿子的情况下,找出儿子中的最大值
let maxIndex = left+1<heapSize && arr[left]>arr[left+1] ? left : left+1;
maxIndex = arr[index]>arr[maxIndex]? index : maxIndex;
if(maxIndex === index) break;
swap(arr,index,maxIndex);
index=maxIndex;
left=2*index+1;
}
}
// 取出大根堆的最大值,并使得剩下的仍然是大根堆
function heapSort(arr){
if(!arr || arr.length<=2) return;
let heapSize = arr.length;
// 排出第一个大根堆
for(let i=0;i<arr.length;i++){
heapInsert(arr,i);
}
swap(arr,0,--heapSize);
while(heapSize>0){
heapify(arr,0,heapSize);
swap(arr,0,--heapSize);
}
}
二 桶排序
1.计数排序
思路:
只能在特定情况下使用,例如对员工的年龄进行排序,首先年龄有大小限制,基本在0-200之间,并且会有一些重复的数字,按0-200的顺序计算每一个年龄的个数来排序
案例:
//计算某年龄数组[10,3,6,8,8,3,10,8]
function countSort(arr){
let arr1 = new Array(10+1).fill(0);
let arr2 = [];
for(let i=0;i<arr.length;i++){
arr1[arr[i]]++;
}
// arr1 =[0,0,2,0,0,1,0,3,0,2]
for(let i=0;i<arr1.length;i++){
while(arr1[i]>0){
arr2.push(i);
arr1[i]--;
}
}
// arr2 = [3,3,6,8,8,8,10,10]
}
2.基数排序
思路:
设置0-9 10个桶,从某一个数组的个位数开始排,以此进桶出桶,先进先出,后进后出
例如 arr [02,13,04,52,06];
首先找出这个数组中最大的十进制位数(此数组是2位);
再计算个位数字的个数
count [0,0,2,1,1,0,1,0,0,0]
index [0,1,2,3,4,5,6,7,8,9]
然后将count数组转为前缀和
count [0,0,2,3,4,4,5,5,5,5]
遍历原数组,找出每一项的个位数,将其作为count的下标,然后找到对应的count[i],比如06对应的count是5,意思就是个位数小于等于6的项,数量为5个,而06作为最后一个项,肯定是后进桶,那么也要后出桶,它一定放在原数组下标为4的位置,然后count[i]--。以此类推
案例:
radixSort1([02,13,04,52,06])
function radixSort1(arr){
if(!arr || arr.length<=2) return;
radixSort(arr,0,arr.length-1,maxBits(arr))
}
// 找出数组中最大的十进制位数
function maxBits(arr){
let maxDigits = 0;
for (let i = 0; i < arr.length; i++) {
const num = Math.abs(arr[i]);
const numDigits = Math.floor(Math.log10(num)) + 1;
if (numDigits > maxDigits) {
maxDigits = numDigits;
}
}
return maxDigits;
}
// 找到某个数的某一位
function getDiget(item,d){
const itemString = Math.abs(item).toString();
const itemArray = itemString.split('').reverse();
return itemArray[d-1] || 0;
}
function radixSort(arr,L,R,maxDigit){
//设桶(count)的长度是10,原数组下标i,count数组下标j,新数组arr1
let radix =10,i=0,j=0,arr1=new Array(R-L+1)
for(let d=1;d<=maxDigit;d++){
let count = new Array (radix).fill(0);
// 计算count
for(i=L;i<=R;i++){
j=getDiget(arr[i],d) // 根据d来找到需要的某位数
count[j]++;
}
// 转为前缀和
for(j=1;j<radix;j++){
count[j]=count[j]+count[j-1];
}
// 遍历原数组,出桶
for(i=R;i>=L;i--){
j=getDiget(arr[i],d);
arr1[count[j]-1] = arr[i];
count[j]--;
}
// copy到原数组中
for(i=0,j=0;i<=R;i++,j++){
arr[i]=arr1[j];
}
}
}
三 排序总结
1.什么是排序的稳定性
就是具有相同性质的两项,在排完序后,相对次序不变
例如: 一个对象中有年龄和班级两项,先按照年龄排序,再按照班级排序,那么最后就是每个班级中年龄都是有序的,纯数字是不存在稳定性的
2.哪些排序可以实现稳定性
冒泡,插入,归并,桶
3.哪些排序不能实现稳定性
快速,选择,堆
4.基于比较的排序的总结
基于比较的排序 | 时间复杂度 |
---|---|
选择 | O(N^2) |
冒泡 | O(N^2) |
插入 | O(N^2) |
归并 | O(N * logN) |
快排(最快最优) | O(N * logN) |
堆 | O(N * logN) |
空间复杂度 | 稳定性 |
--- | --- |
O(1) | 无 |
O(1) | 有 |
O(1) | 有 |
O(N) | 有 |
O(logN) | 无 |
O(1) | 无 |