计数排序与基数排序

计数排序与基数排序

计数排序

计数排序:使用一个数组记录序列中每一个数字出现的次数,将该数组的下标作为实际数据,元素的值作为数据出现的次数。例如对于序列[3,0,1,1,3,3,0,2],统计的结果为:

0出现的次数:2次
1出现的次数:2次
2出现的次数:1次
3出现的次数:3次

依据出现的次数就可以构建出排序之后的序列

void CountSort(vector<int>& nums) {
    int minNum = nums.front();
    int maxNum = nums.front();
    for (int num : nums) {
        minNum = std::min(minNum, num);
        maxNum = std::max(maxNum, num);
    }
    int helpSize = maxNum - minNum + 1;
    int* help = new int[helpSize] {0};//例如最大值为10,最小值为5,需要开辟的数组大小是10-5+1
    for (int num : nums) {
        help[num - minNum]++;//统计次数
    }
    nums.clear();
    for (int i = 0; i < helpSize; i++) {
        while (help[i]--) {
            nums.push_back(i + minNum);
        }
    }
    delete[] help;
}

计数排序的时间复杂度和空间复杂度

时间复杂度:O(N+max-min),其中max为数组中的最大元素,min为数组中的最小元素。计数排序需要遍历原数组一趟,得到原数组的最大值和最小值从而决定需要开辟的辅助数组的大小,还需要遍历辅助数组得到有序序列

空间复杂度:O(max-min),取决于数组中最大值和最小值的差

计数排序适用于数据量很大,但是数据分布比较密集的场景,例如有1亿个数,它们都在[20,1000]范围,此时就可以使用计数排序,与其它排序算法相比(例如快排、冒泡),计数排序是一种非比较排序

基数排序

在计数排序中,对于数据较为分散的场景,所需开辟的额外空间较大,例如序列[1,56,26,9999,7],若使用计数排序,需要额外开辟一个大小为9999的数组,但是原数组只有5个数需要进行排序,在这种情况下,可以考虑使用基数排序

基数排序也是一种非比较排序,基本思想是:先将原序列按照个位进行排序,在按照十位进行排序……依次类推,直到序列完全有序,以[1,56,26,9999,7]为例,流程如下:

计数排序与基数排序_第1张图片

基数排序的轮数取决于序列中最大值的位数,在进行基数排序时,位数小的数字一定在位数大的数字的前面,例如上图中7虽然在进行第一轮排序完成后处于56的后面,但是当完成第二轮排序后7处于56的前面,因为7的十位为0

如何提取一个数字的个位、十位、百位?

方案一:使用to_string将该数字转化为字符串,依次进行提取

方案二:定义一个offset变量,初始为1,将(num/offset)%10,得到个位,每进行一轮,将offset*10

int num = 3675;
int offset = 1;
int bitNum;
while (bitNum=num / offset) {
    cout << bitNum % 10 << ' ';
    offset *= 10;
}
cout << endl;

基数排序的桶

在进行基数排序时一般使用队列作为基数排序的桶,对10进制数字进行排序就需要准备9个队列

void RadixSortByQueue(vector<int>& nums, int bits, int BASE = 10) {//使用队列作为桶进行基数排序
    //bits表示序列中的最大值的位数
    //BASE表示多少进制
    vector<queue<int>> Queues;
    Queues.resize(BASE);
    int offset = 1;
    for (int offset = 1; bits > 0; bits--, offset *= BASE) {//例如最大值为156,则进行3轮
        for (int num : nums) {
            int bitNum = (num / offset) % BASE;//得到个位/十位/百位……的数
            Queues[bitNum].push(num);
        }
        //按照个位/十位/百位……排好的数已经放入Queues中
        nums.clear();
        for (auto& Queue : Queues) {
            while (!Queue.empty()) {
                nums.push_back(Queue.front());
                Queue.pop();
            }
        }
    }
}

基数排序的优化

前缀数量分区:以序列[1,56,26,9999,7]为例,个位数分别是1,6,6,9,7,可以得到的前缀信息如下

个位数<=1的数据数量:1
个位数<=6的数据数量:3
个位数<=7的数据数量:4
个位数<=9的数据数量:5

那么在每一轮按照特定位进行排序时就不需要使用队列,直接开一个和原始数组等规模的辅助数组help即可,将[1,56,26,9999,7]按照个位进行排序,对于数字7,个位为7,由于个位数<=7的数据数量是4,所以7直接放到help数组中下标为3的位置,此时原序列中的7已经放到help数组中,原序列中个位数<=7的数据数量减少一个,变为3,以此类推,直到原序列中的数据全部按照规则转移到help数组中,此时help数组中的数据就是按照位排序好的数据

void RadixSort(vector<int>& nums, int bits, int BASE = 10) {
    //bits表示序列中的最大值的位数
    //BASE表示多少进制
    int* counts = new int[BASE] {0};
    int* help = new int[nums.size()]{ 0 };
    int offset = 1;
    for (int offset = 1; bits > 0; bits--, offset *= BASE) {
        memset(counts, 0, sizeof(int) * BASE);
        for (int num : nums) {
            int bitNum = (num / offset) % BASE;
            counts[bitNum]++;//先统计个数
        }
        for (int i = 1; i < BASE; i++) {
            counts[i] += counts[i - 1];//统计前缀数量
        }
        for (int i = nums.size() - 1; i >= 0; i--) {
            int bitNum = (nums[i] / offset) % BASE;
            help[--counts[bitNum]] = nums[i];
        }
        memcpy(nums.data(), help, sizeof(int) * nums.size());
    }
    delete[] help;
    delete[] counts;
}

为什么需要从后往前遍历向help数组中填数据?

以[1,99,7]为例,在排个位数时,可以从后往前,也可以从前往后,没有影响,个位数排完之后,得到[1,7,99],但是在排十位数时,必须从后往前,否则就会打乱1和7的顺序,因为1和7的十位都是0,十位<=0的数的个数是2,7是最后一个十位<=的数,因此在遍历时需要从后往前遍历排到靠后位置.

基数排序的拓展

如果原序列中存在负数,如何进行基数排序?

将原序列中的所有数字加上最小值的绝对值,在进行基数排序,将排完序的结果在减去原来最小值的绝对值。如果存在溢出问题,需要考虑使用long long类型。

void RadixSortContainMinus(vector<int>& nums, int BASE = 10) {
    int minNum = nums[0];
    for (int num : nums) {
        minNum = std::min(minNum, num);
    }
    int maxNum = 0;
    for (int& num : nums) {
        num -= minNum;
        maxNum = std::max(maxNum, num);
    }
    int bits = 1;
    while (maxNum / BASE) {
        bits++;
        maxNum /= BASE;
    }
    RadixSort(nums, bits, BASE);
    for (int& num : nums) {
        num += minNum;
    }
}

如果需要排序的数字不是十进制,如何使用基数排序实现?

若需要排序的数字不是10进制,只需要修改BASE即可,其它思路一致,例如需要排序的数字是16进制,那么counts数组的大小定为16即可,统计每一位在0~f的数量,依然使用前缀分区技巧

基数排序的时间复杂度

基数排序的时间复杂度为O(m*n),其中m表示原序列中最大值的位数,n表示数据量,因为要根据位数确定排多少轮。基数排序的空间复杂度为O(m+n),需要使用一个help数组和一个counts数组,其中counts数组用于统计个数,help数组用于进行保存这一轮排序完毕的数据.

你可能感兴趣的:(算法,排序算法,数据结构)