评判一个排序算法时除了时间复杂度和空间复杂度之外还要考虑对cache的捕获效果如何,cache友好的排序算法应该对数据的访问相对集中,快速排序相较于堆排序优点就是在于对cache的捕获效果好。
堆排序
思路:堆排序是借助堆结构进行排序的一个过程,在一个堆结构中对任意根节点它总是比它的叶子节点大/小,将结构抽象为完全二叉树你会发现,通过任意根节点可以直接计算出它的叶子节点,因为使用树状结构对每个节点每次更新的时间复杂度是O(log n),需要更新n/2个节点。
时间复杂度:O(n log n )
空间复杂度 O(1) 不稳定
缓存捕获效果差,适合求top k问题,priority_queue使用的就是队结构
堆排序Cpp代码:
void func(vector<int>&nums){
function<void(int,int)>merge=[&](int start,int end){
int child=start;
int father=2*child+1;
while(child<=end){
if(child+1<=end&&nums[child+1]>nums[child]) child++;
if(nums[father]<nums[child]){
swap(nums[father],nums[child]);
child=father;
father=2*child+1;
}
else break;
}
};
int n=nums.size();
for(int i=n/2-1;i>=0;i--){
merge(i,n-1);
}
for(int i=n-1;i>=0;i--){
swap(nums[0],nums[i]);
merge(0,i-1);
}
}
快速排序
思想:每次选取一个节点,比它大的放右边,,比它小的放左边,将问题不断分化,一般情况下认为选取的节点是个中位数,所以只需要log n轮,每轮遍历n/2^i次,这时候认为时间复杂度是O(n log n),但是极端情况下你会发现如果每次选取的数不是最大就是最小的话它和插入排序没啥区别,这时候时间复杂度是O(n ^2)
时间复杂度O(n log n)- O(n^2)
空间复杂度 O(1)
对缓存捕获较好,访问元素遵循局部性原理
快排的优化,选取节点的时候加上五分中位数,快排比较适合处理大规模数据排序
快排Cpp代码
void func(vector<int>&nums,int start,int end){
if(start>=end) return;
int s=start-1;
int e=end+1;
int val=nums[start];//这里选值可以优化
int index=start;
while(index<e){
if(nums[index]==val) {
index++;
}
else if(nums[index]<val){
swap(nums[index],nums[++s]);
index++;
}
else swap(nums[index],nums[--e]);
}
func(nums,start,s);
func(nums,e,end);
}
归并排序
思路:分治思想,把待排序元素一分为二,构造一个有序的新数组,然后递归构建即可,到最后会得到一个有序数组
时间复杂度 O(n log n)
空间复杂度 O(n)
缓存友好,但是排序中高频繁的写操作,让它并不是很高效,应用场景也是有的,当数据量较大甚至内存装不下的适合,归并排序就可以派上用场了,这时候它有一个新的名字外部排序
归并排序Cpp代码:
void func(vector<int>&nums,int start,int end){
if(start>=end) return;
int mid=(start+end)/2;
func(nums,start,mid);
func(nums,mid+1,end);
vector<int>tmp(end-start+1);
int start1=start,start2=mid+1;
int index=0;
while(start1<=mid&&start2<=end){
int val1=start1<=mid?nums[start1]:INT_MAX;
int val2=start2<=end?nums[start2]:INT_MAX;
if(val1>val2) tmp[index++]=nums[start2++];
else tmp[index++]=nums[start1++];
}
for(int i=start;i<=end;i++) nums[i]=nums[i-start];
}
选择排序
思路:每次选择一位,遍历元素找出应该填到这位的索引,最后交换
时间复杂度 O(n^2)
空间复杂度 O(1)
cache不友好
void func(vector<int>&nums){
int n=nums.size();
for(int i=1;i<n;i++){
int index=0;
for(int j=0;j<n-i;j++){
if(nums[index]>nums[j]) index=j;
}
swap(nums[index],nums[n-i]);
}
}
插入排序
思想:每次选取一个向前更新直到可用使前方满足单调性
时间复杂度 O(n^2)
空间复杂度 O(1)
对缓存友好,访问元素遵循局部性原理,作为sort的一部分,适合小数据量排序
void func(vector<int>&nums){
int n=nums.size();
for(int i=1;i<n;i++){
int val=nums[i],j=i-1;
while(j>=0&&nums[j]>val){
nums[j+1]=nums[j--];
}
nums[j+1]=val;
}
}
冒泡排序
思路:每次选取一个位置和别的元素比较,每次遍历确认一位
时间复杂度 O(n^2)
空间复杂度 O(1)
cache不友好
void func(vector<int>&nums){
int n=nums.size();
for(int i=0;i<n;i++){
bool b=true;
for(int j=i+1;j<n;j++){
if(nums[i]>nums[j]) {
b=false;
swap(nums[i],nums[j]);
}
}
if(b) break;
}
}