十大排序算法C++

一、总结

稳定排序:
冒泡排序、插入排序、桶排序、计数排序、归并排序、基数排序;
不稳定排序:
选择排序、希尔排序、堆排序、快速排序

分治:
分而治之,将大问题分为多个相同的小问题,一般通过递归来实现。

参考资料1
参考资料2

二、 算法详细


#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <algorithm>

using namespace std;
const int N = 1e5+10;
int nums[N], help[N];
int n;



1. 归并排序
/*归并排序 - 分治思想,左右数组都是排好序的 然后再合并、统一排序

可以用归并思想求解逆序对问题 

时间复杂度 最好 平均 最坏 O(nlogn) 
空间复杂度 O(n) 
稳定的非原地排序 

采用分治的思想
1. 将数组一分为二,分别把左右数组排序好;
2. 然后再将左右两个数组合并成一个新的数组。 
*/
void mergeSort(int l, int r) {
    if(l >= r)  return ;
    int mid = l+r>>1;
    int i = l, j = mid+1, cnts = 0;
    mergeSort(l, mid), mergeSort(mid+1, r);
    while(i <= mid && j <= r) {
        help[cnts++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
    }
    while(i <= mid) help[cnts++] = nums[i++];
    while(j <= r)   help[cnts++] = nums[j++];
    
    for(int i = 0; i < cnts; ++i) {
        nums[l++] = help[i];
    }
}

2. 快速排序
/* 快速排序 —l,j 有序 j+1, r 中 j左边的数都小于等于nums[j]右边的都大于等于nums[j]

可以用快速排序的思想寻找数组中第k大/小的数 

时间复杂度 最好 平均 O(nlogn) 最坏O(n^2)
空间复杂度 O(logn) 
不稳定的原地排序 

1. 基准值:利用分治的思想,先在数组中取一个基准pivot;
2. 分割:根据基准值重新排序数组;
3. 递归:递归排序子数组,递归结束条件为判断子列数组大小是0/1 
*/
void quickSort(int l, int r){
    if(l >= r)  return;
    int x = nums[l+r>>1], i = l-1, j = r+1;
    while(i < j){
        do i++; while(x > nums[i]);
        do j--; while(x < nums[j]);
        if(i < j)
            swap(nums[i], nums[j]);
    }
    quickSort(l, j), quickSort(j+1, r);
}

3. 选择排序
/*选择排序 - 每轮都选择剩余元素中 最小的 和 第一个 进行交换,不会发生元素移动 

时间复杂度 最好 最坏 平均 O(n^2)
空间复杂度 O(1) 
不稳定的原地排序 

1. 将数组分为已排序、未排序;
2. 不断地将未排序区间中最小的元素放入已排序区间的尾部。 
*/
void selectSort() { 
    
    for(int i = 0; i < n-1; ++i) {
        int cur_min = i;
        for(int j = i+1; j < n; ++j) {
            if(nums[j] < nums[cur_min]) {
                cur_min = j;
            }
        }
        swap(nums[i], nums[cur_min]);
    }
}

4. 插入排序
/*插入排序 - 待序元素插入有序元素 因此需要在搜索位置的同时 后移有序元素

与选择排序不同的是,属于数据移动,而非数据交换。 

时间复杂度 最好 O(n)最坏平均 O(n^2)
空间复杂度 O(1) 
可以是稳定的原地排序
 
1. 将数组分为已排序、未排序两个动态区间;
2. 将未排序区间中数据插入到已排序区间中,会涉及到数据的移动。 
*/
void insertSort() {
    //从i开始是默认i == 0已经是有序    
    for(int i = 1; i < n; ++i) {
        auto value = nums[i];//必须备份 因为会覆盖
        int j = i-1;
        while(j >= 0 && nums[j] > value) {
            nums[j+1] = nums[j];
            --j;
        }
        nums[j+1] = value;
    }
}

5. 冒泡排序
/*冒泡排序 - 实际上是每轮就确定一个最大的元素(如果按照从小到大排列)

时间复杂度 最好 O(n)最坏平均 O(n^2)
空间复杂度 O(1) 
可以是稳定的原地排序
 
1. 比较相邻两个元素的大小,不满足排序要求则交换。 
*/
void bubbleSort() {
    for(int i = 0; i < n-1; ++i) {
        bool flag = false;//这样写在最佳情况下的时间复杂度才会是O(n)
        for(int j = 0; j < n-i-1; ++j) {//优化
            if(nums[j] > nums[j+1]) {
                swap(nums[j], nums[j+1]);
                flag = true;
            }
        }
        if(!flag)   break;
    }
}

/*
基于元素之间的比较和交换被称为比较类排序。
	nlogn时间复杂度的算法称为非线性时间比较类排序; 
接下来介绍线性时间比较类排序。 
*/


6. 计数排序
/*
计数排序 - 适合数据比较集中的排序,将元素转换成键值
不是基于比较的排序算法

时间复杂度 最好 O(n+k)
空间复杂度 O(k) 
稳定的非原地排序

1. 其核心在于将输入的数据值转化为键存储在额外开辟的数组空间里; 
2. 在数组空间中记录出现的次数,并对所有计数进行累加;
3. 反向填充数组,按照频数取出,每取出一个元素就将count[i]-1。 

个人理解实际上就是 对应第k个就放到下标为k-1的位置 

*/
void countSort() {
    auto max_value = *max_element(nums, nums+n);
    auto min_value = *min_element(nums, nums+n);
    vector<int> count(max_value - min_value + 1);
    
    for(int i = 0; i < n; ++i) {
        ++count[nums[i]-min_value];
    }
    
    /*这样写就结束的话 无法保证数据的稳定性 因为覆盖原数组
    int indx = 0;
    for(int i = 0; i < count.size(); ++i) {
        for(int j = 0; j < count[i]; ++j) {
            nums[indx++] = i + min_value;
        }
    }
    */
    
    //因此 -> 从后面 反向填充数组:相等的先填充后面的,然后减少计数值
    for(int i = 1; i < count.size(); ++i) {
        count[i] = count[i-1] + count[i];
    }
    
    
    int res[n];
    for(int i = n-1; i >= 0; --i) {
        auto cnts = count[nums[i]-min_value];
        res[cnts-1] = nums[i];
        --count[nums[i]-min_value];
    }
    memcpy(nums, res, sizeof res);
}

7. 基数排序
/*基数排序 - 适合给位数较多的数字进行排序 (最大数字多少位就只需要准备几个桶)

适合手机号、较长的英文专业名词排序

时间复杂度:最好最坏平均 O(n*k) 
空间复杂度:O(n)
非原地稳定排序 

1. 找到数组中最大数字位数,即最大位数bits;
2. 初始化count存储余数0~9的桶;
3. 运用计数排序思想对数组进行排序,循环bits次; 

*/void radixSort() {
    auto max_value = *max_element(nums, nums+n);
    int bit = 0, tmp = max_value;
    while(tmp) {
        tmp /= 10;
        ++bit;
    }
    int radix = 1;
    vector<int> count(10);
    int res[n];
    
    for(int i = 1; i <= bit; ++i) {
        memset(res, 0, sizeof res);
        
        for(int j = 0; j < n; ++j) {
            ++count[nums[j]/radix%10];
        }
        
        for(int j = 1; j < 10; ++j) {
            count[j] = count[j-1] + count[j];
        }
        
        
        int indx = 0;
        for(int j = n-1; j >= 0; --j) {
            int cnts = count[nums[j]/radix%10];
            res[cnts-1] = nums[j];
            --count[nums[j]/radix%10];
        }
        memcpy(nums, res, sizeof res);
        
        radix *= 10;
    }
}

8. 桶排序
/*桶排序 - 每个桶内部排序后再统一排序

如果桶的个数 等于了数组元素的个数,那么他就是计数排序。
如果数组元素为n,m个桶,那么每个桶的元素个数k=n/m,如果桶内使用快速排序;

时间复杂度 O(nlog(n/m)),具体需要开桶的个数,最好平均 O(k) 最坏O(n^2) 
空间复杂度 O(k) 
非原地排序 具体稳定性要看使用的内部算法 

1. 整体有序:将数组分配到有限量的桶中; 
2. 局部有序:对每个桶进行排序; 

*/void bucketSort() {
    int k = 10;
    auto max_value = *max_element(nums, nums+n);
    auto min_value = *min_element(nums, nums+n);
    int bucket_num = (max_value - min_value) / k + 1;
    vector<int> bucket[bucket_num];
    
    for(int i = 0; i < n; ++i)  bucket[(nums[i]-min_value)/k].emplace_back(nums[i]);
    
    for(int i = 0; i < bucket_num; ++i) sort(bucket[i].begin(), bucket[i].end());
    
    int indx = 0;
    for(int i = 0, j = 0; i < bucket_num; ) {
        if(j < bucket[i].size())    nums[indx++] = bucket[i][j++];
        else    i++, j = 0;
    }
    
}
/* 第二种写法 
void bucketsort(vector<int>& arr) {
    queue<int> buckets[10];
    for (int digit = 1; digit <= 1e9; digit *= 10) {
        for (int elem : arr) {
            buckets[(elem / digit) % 10].push(elem);
        }
        int idx = 0;
        for (queue<int>& bucket : buckets) {
            while (!bucket.empty()) {
                arr[idx++] = bucket.front();
                bucket.pop();
            }
        }
    }

作者:Coderoger
链接:https://www.acwing.com/blog/content/2064/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
*/


9. 堆排序
/*堆排序 - 和别的不一样 小标从1开始方便维护,每次都维护堆,将维护完成的放到数组末尾

时间复杂度 最好最坏平均 O(nlogn) 
空间复杂度 O(1) 
不稳定的原地排序 

堆总是一个完全二叉树,
任意节点都必须大于等于或者小于等于它的子节点
(分别对应大顶堆、小顶堆

一、建堆: 
1. 将数组抽象为一个堆,从下标0~n-1从完全二叉树的根节点开始对应;
2. 从下标n/2位置不断的向前移动,交换元素直至满足完全二叉树定义;
二、维护堆:
3. 堆顶元素就是排序结果,因此每次将堆顶元素与最后一个元素进行交换, 
即可取出堆顶元素,同时sz--,再down操作维护堆。 

*/
void down(int u, int sz) {
    int tmp = u;
    while(2*u <= sz && nums[2*u] < nums[tmp])   tmp = 2*u;
    while(2*u+1 <= sz && nums[2*u+1] < nums[tmp])   tmp = 2*u+1;
    if(tmp != u) {
        swap(nums[tmp], nums[u]);
        down(tmp, sz);
    }
}
void up(int u) {
    while(u/2 && nums[u/2] > nums[u]) {
        swap(nums[u/2], nums[u]);
        u /= 2;
    }
}
void heapSort() {
    //建堆
    for(int i = n/2; i; --i)   down(i, n);

    for(int i = n; i > 1; --i) {
        swap(nums[1], nums[i]);
        down(1, i-1);
    }
}
10. 希尔排序
/*希尔排序 - 在插入排序一步步移动的基础上优化步长

时间复杂度 最好O(nlogn) 最坏平均 O(nlogn*logn) 
空间复杂度 O(1) 
不稳定的原地排序 
*/
void shellSort() {
    
    for(int gap = n/2; gap; gap /= 2) {
        for(int i = gap; i < n; ++i) {
            int tmp = nums[i], j = i;
            for( ; j >= gap && tmp < nums[j-gap]; j -= gap) {
                nums[j] = nums[j-gap];
            }
            nums[j] = tmp;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值