排序算法(五) 三种非比较排序

计数排序

思路

很简单,就是设一个哈希表来统计数组各元素的个数,统计完毕后计算前缀和,将各元素统计数转化为应该放的下标位置,最后遍历原数组各元素,将元素放到哈希表映射的位置即可。(注意每次放一个元素对应下标位置计数减1)

实现

class Solution {
public:
    // 数据范围 -50000 <= A[i] <= 50000, 故数组要容纳100000个数
    int SIZE = 100001;
    // 设置偏移量的目的是使得所有值都为正值,防止数组溢出
    int OFFSET = 50000;
    vector<int> countingSort(vector<int>& nums) {
        vector<int> count(SIZE,0);
        int n = nums.size();
        for(int i = 0; i < n; i++){
            count[nums[i] + OFFSET]++;
        }

        for(int i = 1; i < SIZE; i++){
            count[i] += count[i - 1];
        }

        vector<int> temp(nums.begin(),nums.end());
        for(int i = n - 1; i >= 0; i--){
            int idx = count[temp[i] + OFFSET] - 1;
            count[temp[i] + OFFSET]--;
            printf("idx=%d nums[%d]=%d\n",idx,i,temp[i]);
            nums[idx] = temp[i];
        }
        return nums;
    }
};

分析

时间复杂度: O ( N + k ) O(N+k) O(N+k) , 其中N为数组的长度,k为哈希表的长度

空间复杂度: O ( N + k ) O(N+k) O(N+k), 需要额外用到一个哈希表和一个额外数组

稳定性:稳定,如果你最后遍历数组是后往前遍历的话。

最后遍历数组时一定要从后往前遍历,因为如果一个相同值在数组中出现了多次,统计这些元素是按从前往后的顺序来叠加计数的,通过前缀和换算以后,值最初的下标位置对应就是最后出现的相同元素,排定一个元素下标就减1,指向这个值在原数组中的前一个相同元素,因此我们也要按计数顺序来做出调整,使得同一个值最后出现的元素进入排定数组最后的位置,如果弄成从前往后遍历,那就是先把最后出现的元素反而先放进排定数组最前的位置了,丧失稳定性。

初始次序:

​ 移动次数无关:和归并一样,排定来源来自于额外的数组,有不有序都要移动,一次排定。

​ 比较次数无关:非比较算法

​ 时间复杂度无关: 最好最坏都是O(N+k)

​ 排序趟数无关:O(N) 算法,有不有序都是一趟过

基数排序

(别和计数混淆,计数是Counting Sort基数是Radix Sort)

计数排序的缺点是,计数器的大小严重依赖于原始序列,当元素值极大时将造成极大的空间损耗(数少值大的情况下就是极大浪费空间),改进方法是按各元素位数自底向上来排序,计数器的大小也就被固定在0-9的范围内,显然要付出时间换空间的代价。

一般来说,当待排序序列值域小,数量大时使用计数排序,而当待排序序列值域大,数量小时使用基数排序。

class Solution {
public:
    int OFFSET = 50000; // -50000 <= A[i] <= 50000
    vector<int> RadixSort(vector<int>& nums) {  
        int n = nums.size(),maxlen = 0;
        for(int i = 0; i < n; i++){
            nums[i] += OFFSET;  // 加上偏移量,防止负数影响
            maxlen = std::max(maxlen,nums[i]);  // 取最大元素的位数做循环量
        }

        vector<int> cnt(10,0),tmp(nums.begin(),nums.end());
        int div = 1;
        while(div <= maxlen){
            for(int i = 0; i < n; i++){
                int bit = (nums[i] / div) % 10; //从元素的个位一直取到顶位
                cnt[bit]++;
            }
            for(int i = 1; i < 10; i++){
                cnt[i] += cnt[i - 1];
            }
            for(int i = n - 1; i >= 0; i--){
                int bit = (tmp[i] / div) % 10;
                int idx = cnt[bit] - 1;
                cnt[bit]--;
                nums[idx] = tmp[i]; 
            }
            // 清空计数器, 更新tmp,进行下一位数的排序
            cnt = vector<int>(10,0);
            tmp = vector<int>(nums.begin(),nums.end());
            div *= 10;
        }

        for(int i = 0; i < n; i++){
            nums[i] -= OFFSET;	//排完序别忘了还原原值
        }
        return nums;
    }
    int getElemLen(int digit){
        int cnt = 0;
        while(digit){
            digit /= 10;
            cnt++;
        }
        return cnt;
    }

};

分析

时间复杂度: O ( N ∗ k ) O(N*k) O(Nk), 其中N为数组的长度,k为序列中最大值的位数

空间复杂度: O ( N ) O(N) O(N) , 计数器长度固定为10,可以忽略, 主要看额外数组的长度

稳定性: 稳定,理由同计数排序

初始序列:

​ 移动次数无关:同计数排序,排序依靠额外数组一次排定,无需移动

​ 比较次数无关:同计数排序,排序依靠额外数组一次排定,无需比较

​ 时间复杂度无关:最好最坏情况都是 O ( N ∗ k ) O(N*k) O(Nk)

​ 排序趟数无关:外循环次数取决于最大元素的位数而不是初始序列

桶排序

将原序列按照值域分为多个等长区间的“桶”,将序列各元素分类到对应的桶中并排序,装桶结束后将各桶内元素按顺序逐一“倒回”原数组:

class Solution {
public:
    int OFFSET = 50000; // -50000 <= A[i] <= 50000
    vector<int> sortArray(vector<int>& nums) {  
        buckSort(nums);
        return nums;
    }
    void buckSort(vector<int> &nums){
        int n = nums.size(),maxi = 0,mini = 0;
        for(int i = 0; i < n; i++){
            nums[i] += OFFSET;  // 加上偏移量,防止负数影响
            maxi = std::max(maxi,nums[i]);
            mini = std::min(mini,nums[i]);
        }

        int gap = 2; // 设各桶的区间长,区间设置为左闭右开
        int buckAmount = (maxi - mini) / gap + 1;    //桶个数
        vector<vector<int>> buckets(buckAmount); 
        //元素入桶,每次更新桶时都需要排序,这里采用插入排序
        for(int i = 0; i < n; i++){
            int idx = (nums[i] - mini)  / gap;
            buckets[idx].push_back(nums[i]);
            insertionSort(buckets[idx]);
        }

        int k = 0;
        //将排序后的元素逐一出桶
        for(int i = 0; i < buckAmount; i++){
            int buckLoad = buckets[i].size();
            for(int j = 0; j < buckLoad; j++){
                nums[k++] = buckets[i][j];
            }
        }

        //恢复数组原值
        for(int i = 0; i < n; i++){
            nums[i] -= OFFSET;
        }
    }
    void insertionSort(vector<int> &nums){
        int n = nums.size();
        for(int i = 1; i < n; i++){
            int tmp = nums[i];
            int j = i;
            while(j > 0 && nums[j - 1] > tmp){
                nums[j] = nums[j - 1];
                j--;
            }
            nums[j] = tmp;
        }
    }


};

分析

时间复杂度: 取决于桶的数量k和采用的排序算法。设数组长为n, 最好情况下,各元素被均匀的分到各桶中且都有序,时间复杂度达 O ( N ) O(N) O(N) , 最坏情况下所有元素都挤在一个桶内, 时间复杂度为 O ( N 2 ) O(N^2) O(N2)

本例中采用的排序算法是插入排序,如果采用其他排序算法则需要另行讨论,例如:

排序算法最终复杂度
插入最好$ O(n/k)$ -> O ( N ) O(N) O(N), 最坏O( (n/k)^2 ) $-> $ O ( N 2 ) O(N^2) O(N2)
快排最好 O ( ( n / k ) ∗ l o g ( n / k ) ) O( (n/k)*log(n/k) ) O((n/k)log(n/k)) -> O ( N l o g N ) O( NlogN) O(NlogN), 最坏 O((n/k)^2) -> O ( N 2 ) O(N^2) O(N2)

不过在桶的数量合适的情况上,O(NlogN) 约等于 O(N)。

空间复杂度:取决于桶数组的实现方式,本例采用了vector来动态插入数组,类似于链表,空间复杂度为O(n+k), 也就是数组长度+桶数, 如果采用数组固定分配,则空间复杂度为O(nk)

稳定性:装桶的逻辑类似于计数排序,所以过程是稳定的,千万别用非稳定的排序算法来排序桶。

初始次序:

​ 移动次数:无关,排序源来自于桶数组,一次排定,无需移动

​ 比较次数:无关,非比较算法

​ 时间复杂度:取决于排序桶的排序算法

​ 排序趟数:取决于排序桶的排序算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值