十大排序总结优化-c++

基础知识

  • 比较类排序:通过比较确定元素次序, 时间复杂度不会低于O(nlgn)
  • 非比较类排序:不通过比较确定次序,通常是用空间换时间,能够达到O(n)的时间复杂度,也成为线性时间非排序比较。
  • in-place / out-place:区别在于是否 使用额外的数组 辅助排序
  • 稳定排序:数组中相等的元素在排序后的相对顺序不变

比较类排序(以升序为例)

1. 冒泡排序

  • 算法思想:比较相邻元素,如果第一个元素大于第二个元素,则进行交换
  • 算法分析:最坏复杂度O(n2), 稳定排序,in-place
  • 优化:内循环设置flag, 如果该循环没有发生swap, 说明达到了最优,停止排序
void bubbleSort(vector<int>& nums) {
        int len = nums.size();
        for(int j = 0; j < len - 1; j++){
            bool flag = true;
            for(int i = 0; i < len - j -1; i++){
                if(nums[i] > nums[i+1]){
                    swap(nums[i], nums[i+1]);
                    flag = false;
                }
            }
            if(flag) return ; //说明达到了最优
           
        }
      
    }

但显然,每一次循环都有可能发生多次交换,我们可以记录未排序的数组中的最大值索引,然后在内循环结束后把最大值放入对应位置即可,只需要一次交换。基于这个思想,选择排序被提出。

2. 选择排序

  • 基本思想:每次循环选择最大值放入数组尾部
  • 分析:不稳定排序,最坏时间复杂度O(n2), in-place
  • 为什么不稳定: 比如 7 1 2 5 3 5 8 , 7 和第二个5交换位置,会破坏稳定性。
void selectSort(vector<int>& nums) {
        int len = nums.size();
        for(int j = 0; j < len - 1; j++){
            int max = 0;
            //记录最大值的索引
            for(int i = 0; i < len - j ; i++){
                max = (nums[i] > nums[max]) ? i: max;
            }
            if(max != len - j -1) 
                swap(nums[max], nums[len - j - 1]);
        }
        
    }      

我们把数组看作已排序和未排序两部分,则冒泡和选择都是从未排序部分中选择中最值,然后放入已排序部分的边界位置。同样的,我们也可以直接选择一个未排序元素,找到它在已排序部分中的位置,插入进去。插入排序就是这种思想。

3.插入排序

  • 基本思想: 在0 - i-1元素有序的情况下,依次将第i个元素插入前面的有序数组, 同时将大于它的数组元素后移
  • 分析:最坏时间O(n2), 稳定排序,in-place
 void insertSort0(vector<int>& nums) {
        int len = nums.size();
        for(int i = 1; i < len ; i++){
            if(nums[i] >= nums[i-1]) continue;
            int temp = nums[i];
            //在0- i-1中查找nums[i]应该插入的位置
            int t  = 0;
            for( t = i-1; t >=0 && temp < nums[t] ; t--){
                nums[t+1] = nums[t];
            }
            nums[t+1] = temp;
        }
        
    }
  • 优化 : 对于大规模数据,我们也可以使用二分查找来确定要插入的位置。但这种模式并没有优化时间复杂度,因为后续的数组元素向后移动,依然需要O(n)的时间。
 void insertSort(vector<int>& nums) {
        int len = nums.size();
        for(int i = 1; i < len ; i++){
            if(nums[i] >= nums[i-1]) continue;
            //在0- i-1中查找nums[i]应该插入的位置
            //可用二分查找,但总体效率不会变快
            int l = 0, r = i - 1;
            int mid = 0;
            while(l < r){
                mid = l + (r - l)/2; 
                if(nums[mid] <= nums[i]) l = mid + 1;
                else if(nums[mid] > nums[i]) r = mid;
            }
            //插入到nums[l]的位置
            //移动元素
            int min = nums[i];
            for(int t = i; t > l; t--){
                nums[t] = nums[t-1];
            }
            nums[l] = min;
        }
        
    } 

插入排序在对几乎已经排好序的数据操作时,效率高,最优复杂度为O(n);
但其一般情况下是低效的,所以可以用较低的代价让数组尽量有序,降低调用插入排序的代价。也就是下面的希尔排序。
基于这个原因,我们也可以使用快排+ 插入排序相结合的方式,在快排划分子数组到一定规模,继而使用插排,这也是stl的sort的一个实现思想。

4.希尔排序

  • 基本思想:插入排序的优化版本,也成为递减增量排序算法。将间隔为gap的元素视为一组数据,调用插入排序; gap 从len/2 逐渐减半至1,每一个gap执行一次插入排序。
  • 分析: 时间复杂度再O(nlgn) ~O(n2)之间,非稳定排序,in-place
void shellSort(vector<int>& nums) {
        int len = nums.size();
        for(int gap = len /2; gap >= 1; gap /= 2){
            //间隔gap 的一次插入排序
            //gap = 1时,就相当于普通的插入排序,这时数据基本有序,所以执行会很快
            for(int i = gap; i < len ; i++){
                int temp = nums[i];
                int r = i - gap; 
                while(r >= 0 && temp < nums[r]){
                    nums[r + gap] = nums[r];
                    r -= gap;
                }
                nums[r + gap] = temp; 
            }
        }
        
    }      

5.归并排序

  • 基本思想 : 分治算法,递归划分数组直到只有一个元素,然后再逐级合并相邻两个有序子数组
  • 分析:最坏时间O(nlgn), 稳定,out-place, 空间复杂度O(n)
     //两个有序数组合并 [l, q] [q + 1, r]
   void merge(vector<int>& nums, int l, int q, int r){
        vector<int> temp;
        int index1 = l, index2 = q+1;
        while(index1 <= q && index2 <= r){
            if(nums[index1] <= nums[index2]){
                temp.push_back(nums[index1++]);
            }else
                temp.push_back(nums[index2++]);
        }
        while(index1 <= q){
            temp.push_back(nums[index1++]);
        }
        while(index2 <= r){
            temp.push_back(nums[index2++]);
        }
        //数据转移
        int index = 0;
        for(int i = l; i <= r; i++){
            nums[i] = temp[index++];
        }
    }
    //递归框架
    void partition(vector<int>& nums, int left, int right){
        if(left == right) return ;
        int q = left + (right - left)/2;
        partition(nums, left, q);
        partition(nums, q + 1, right);
        merge(nums, left, q, right);
    }
    void mergeSort(vector<int>& nums) {
        partition(nums, 0, nums.size() -1);
    }

6.快速排序(重点!)

  • 基本思想 : 分治思想,是应用中使用最多的排序。对于一个数组,选择其中一个元素作为主元,根据与主元的关系将数组划分为左右两个区间(左区间<=主元,右区间> 主元),递归划分直到区间元素为0.
  • 分析 : 时间O(nlgn), 不稳定排序,in-place
  • 注意点: 划分左右区间时,不要把主元再放入子区间,不然可能会发生死循环。
 void quitSort(vector<int>& nums, int left, int right) {
        if(left >= right) return ;
        int key = nums[right];
        //划分左右子区间
        int p = left ;
        for(int i = left; i < right; i++){
            if(nums[i] <= key){
                swap(nums[i] , nums[p++]);
            }
        }
        cout<<p <<" "<< right<<endl;
        swap(nums[p], nums[right]);
        quitSort(nums, left, p -1);  //注意时p-1!!!
        quitSort(nums, p+1, right);
    }
  • 优化 : 选取最后一个元素作为主元,对于有序数组来说,时间复杂度为O(n2)。 因此我们可以随机选取主元来避免该情况:
int index = p + rand()%(r - p + 1);
//记得换到最后一个上
swap(nums[index], nums[r]);

但是随机选取对于都是重复数字(比如都是2)的数组来说,依然没有优化作用。更近一步,我们可以选择三路快排的方法来优化同一元素很多的情况,即三值取中, 左边是< x, 中间是 == x, 右边是> x 的元素:

    //三路快排
    pair<int, int> Partition3(vector<int>& nums, int p, int r){
        // //随机选取主元
        int index = p + rand()%(r - p + 1); 
        swap(nums[index], nums[r]);
        //三路
        int i = p -1;
        int k = r ;
        int x = nums[r];
        for(int j = p; j < r && j < k ; j++){
            if(nums[j] < x){
                i++;
                swap(nums[i], nums[j]);
            }else if(nums[j] > x){
                k--;
                swap(nums[k], nums[j]);
                j--;  //注意这个,比如 5 1 1 2 0 0, 可能交换后,nums[j]依然是大于x的
            }
        }
        swap(nums[k], nums[r]);//与nums[k]交换
        return make_pair(i, k + 1);
    }
    void QSort(vector<int> & nums, int left, int right){
        if(left >= right)
            return ;
        pair<int,int> q = Partition3(nums, left, right);
        QSort(nums, left, q.first);
        QSort(nums, q.second, right);
    }
  • 那么c++ stl里面的sort 是怎么实现的呢?
    简单来说,先调用快排进行分段,如果该段区间元素小于一定阈值(16),用插排,如果递归深度达到一定阈值(2*lg(n)),停止继续递归,当前子区间用堆排序。
    sort具体源码分析
  • 快排的复杂度为什么是O(nlgn)?
    在这里插入图片描述

ref:https://harttle.land/2015/09/27/quick-sort.html

7. 堆排序

  • 基本思想 : 通过构建大根堆,每次输出一个最大值,循环n遍
    • 分析 : 时间O(nlgn), 不稳定,in-place

    //建立大根堆, 从下标0开始
    void heapBuild(vector<int> &nums){
        int len = nums.size();
           //从底向上
        for(int i = (len-1)/2; i >= 0; i--)
            maxHeapify(nums, i, len);
    }
    
    //维护以i为根, 长度为n的大根堆性质(可原地排序)
    void maxHeapify(vector<int> &nums, int i, int len ){
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int large = i;
        if(left < len && nums[left] > nums[i]){
            swap(nums[left], nums[i]);
            large = left;
        }
        if(right < len && nums[right] > nums[i]){
            swap(nums[right], nums[i]);
            large = right;
        }
        //注意判断large != i, 否则会死循环
        if(large != i){
            maxHeapify(nums, large, len);
        }
    }
    void heapSort(vector<int>& nums){
        heapBuild(nums);
        int len = nums.size() ;
        while(len > 0){
            swap(nums[0], nums[len - 1]);
            len--;
            maxHeapify(nums, 0, len);
        }
    }

基于比较的排序如上,最好的时间复杂度是O(nlgn), 无法到达线性时间,而下面的非比较类排序,则可以突破这个问题。

非比较类排序(以升序为例)

8.计数排序

  • 基本思想 : 创建数组,统计nums中各数值出现次数,然后依次将元素复制到nums中的对应位置
  • 分析 : 时间O(n + k), 稳定,out-place, 空间O(k+n), k为数组取值范围
  • 优化 : 用map计数会节省一些空间O(n)
    void  countSort(vector<int>& nums){
        int len = nums.size() ;
        map<int,int> mp;
        for(int num : nums){
            mp[num]++;
        }
        int i = 0;
        map<int, int>::const_iterator it = mp.begin();
        for(; it != mp.end(); it++){
            int key = it->first;
            int count = mp[key];
            while(count--){
                nums[i++] = key;
            }
        }
    }

9.桶排序

  • 基本思想 : 将原数组元素分到有限数量的桶中,然后分别对每一个桶排序
  • 分析 : 时间O(n + k), 稳定,out-place, 空间O(k+n), k为数组取值范围
  • 注意 : 桶的个数必须大于等于1, 所以需要+1
   void  bucketSort(vector<int>& nums){
        int n = nums.size();
        // 获取数组的最小值和最大值
        int maxNum = nums[0], minNum = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > maxNum) maxNum = nums[i];
            if (nums[i] < minNum) minNum = nums[i];
        }
        //初始化桶
        int bucketNum = 5, bucketSize = (maxNum - minNum)/bucketNum + 1;  //最少有一个桶!!
        vector<vector<int>> buckets(bucketNum);
        for(int num : nums){
            int index = (num - minNum)/bucketSize;
            buckets[index].push_back(num);
        }
        //对每一个桶排序
        for(int i = 0; i < bucketNum; i++){
            sort(buckets[i].begin(), buckets[i].end());
        }
        //写入nums
        int j = 0;
        for(int i = 0; i < bucketNum; i++){
            for(int num : buckets[i]){
                nums[j++] = num;
            }
        }

    }

10.基数排序

  • 基本思想 : 按照位数,从低到高依次排序,每一次都是基于上一次的排序结果,所以保持稳定
  • 分析 : 时间O(nm), 稳定,out-place, 空间O(k+n), m为数值的最大位数
  • 注意 : 要求数值是非负整数,所以如果有负数,可以加上一个值,进行数据处理。 以及如何按照某一位的数据进行排序的方法如何实现(计数排序)。
   //按照某一位排序(计数排序)
    void radix(vector<int>& nums, vector<int>& temp, int divisor){

        vector<int> count(10, 0);
        int n = nums.size();
        for(int num : nums){
            //取得该位上的数字
            int x = (num/divisor)%10;
            if(x != 9) count[x + 1]++;
        }
        //计算前缀和, count[i]代表i对应的起始位置
        for(int i = 1; i < 10; i++){
            count[i] += count[i-1];
        }
        for(int num : nums){
            int x = (num/divisor)%10;
            temp[count[x]] = num;
            count[x]++;
        }
        
    }
    void  radixSort(vector<int>& nums){
        int n = nums.size();
        // 预处理,让所有的数都大于等于0
        for (int i = 0; i < n; ++i) {
            nums[i] += 50000; // 50000为最小可能的数组大小
        }
        // 找出最大的数字,并获得其最大位数
        int maxNum = nums[0];
        for (int i = 0; i < n; ++i) {
            if (nums[i] > maxNum) {
                maxNum = nums[i];
            }
        }
        int num = maxNum, maxLen = 0;
        //求最大位数
        while(num){
            maxLen++;
            num /= 10;
        }
        vector<int> temp(n, 0);
        //从低位到高位排序
        int divisor = 1;
        for(int i = 0; i < maxLen; ++i){
            radix(nums, temp, divisor);
            swap(temp, nums);
            divisor *= 10;
        }
        //减去与处理量
        for (int i = 0; i < n; ++i) {
            nums[i] -= 50000; // 50000为最小可能的数组大小
        }
   }

汇总

图片
图片来源下面的参考链接1.

参考链接

  • https://leetcode.cn/problems/sort-an-array/solution/by-peaceful-thompsonfsu-b3bu/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值