三个非比较排序(线性排序)

1、计数排序:


条件:基本上仅用于数字排序,需要知道数据的上下限,而且最好从0开始这样实现起来比较方便些,需要[上限 - 下限]的空间复杂度,时间复杂度可以按教科书的认知是O(N),实际操作起来其实要随数据范围的变大而变大(O(N * 2) + O(范围长度)),如数据范围特别大从0-2^32,这样的话计数排序的时间复杂度也相当不小。

结论:适合范围不太大的数据的排序,最好从0到正整数这样的范围

思路:比如排序1000个数,范围为0-99之间,即上限99下限0,计数排序的思路是:

1、既然范围已知,创建一个100个元素大小的数组,然后遍历一次全部1000个数据,记录每个数据的出现次数

2、然后对0-99范围内每个数字,累计全部1000个数据中,小于等于这个数字的数据个数,更新到上一步创建的数组中,如:

假设全部1000个数据中,0出现12次,1出现15次,2出现0次,3出现5次,4出现17次,5出现10次,则:

数组[0] = 12,数组[1] = 12 + 15 = 27,数组[2] = 27 + 0 = 27,数组[3] = 27 + 5 = 32,数组[4] = 32 + 5 = 37,数组[5] = 37 + 10 = 47

这步遍历一次这个数组,时间复杂度为O(数据范围长度),目的是排范围中每个数字的排序座次,比如从小到大排序,则全部数据中的5将出现在38-47位

3、然后就按第2步更新好的数组,再逆向遍历一次全部1000数据,按每个数字的出现座次落位到返回结果数组中


代码:

#include <iostream>
#include <functional>
#include <random>
#include <vector>
#include <algorithm>


int cutf (int max, int cur) {
    return cur/max;
}
using CutFunc = std::function<int (int, int)>;


class BucketSort {
    int *rawdata;
    int size;
    int max;
    int bucket_num;
    std::vector<int> *buckets;
    std::function<int (int, int)> &cutfunc;


public:
    BucketSort (int *_data, int _size, int _bucket_num, int _max, CutFunc &_cutfunc):size(_size), bucket_num(_bucket_num), max(_max), rawdata(_data), cutfunc(_cutfunc) {
        buckets = new std::vector<int>[bucket_num];
    }


    ~BucketSort () {
        delete []buckets;
    }


    void bucketsort () {
        for (int i = 0; i < size; i++) {
            int idx = cutfunc(max, rawdata[i]);
            buckets[idx].push_back(rawdata[i]);
        }


        for (int i = 0; i < bucket_num; i++) {
            std::sort(buckets[i].begin(), buckets[i].end());
        }


        for (int i = 0; i < bucket_num; i++) {
            for (int j = 0; j < buckets[i].size(); j++) {
                std::cout << buckets[i][j] << "\t";
            }
        }
        std::cout << std::endl;
    }


    int Get_Median () {
        int half = size/2, sum = 0;
        for (int i = 0; i < bucket_num; i++) {
            int cur = buckets[i].size();
            if (sum + cur < half) {
                sum += cur;
            } else if (sum + cur == half) {
                return buckets[i][buckets[i].size() - 1];
            } else {
                return buckets[i][sum + cur - half];
            }
        }
    }
};


int main () {
    std::random_device rd;
    int data[100];
    int max = 10000;
    for (int i = 0; i < 100; i++) {
        int cur = rd() % max;
        std::cout << cur << ",\t";
        data[i] = cur;
    }
    std::cout << std::endl;


    int bucket_num = 100;
    CutFunc cutfunc = &cutf;
    BucketSort bs(data, sizeof(data)/sizeof(data[0]), bucket_num, max, cutfunc);
    bs.bucketsort();
    std::cout << bs.Get_Median() << std::endl;
    return 0;
}



2、桶排序:


桶排序可以认为是计数排序的加强版,计数排序很机械的强牵制于数据出现范围的大小,对范围敏感,范围太大导致低效;

桶排序和计数排序相似的是,它也需要关注数据出现范围,但不再是挨个关注每个数字出现的次数,而是按数字的范围,用自定义的数据划分方式,将原始数据划分在一系列的桶内,比如原始数据的范围是0-9999,那么可以自定义100个桶,划分方式就是0-99在桶1,100-199在桶2.....直到9900-9999在桶100,这样首先保证了桶之间有序,然后依次对排序每个桶内的数据,最后就可以按桶id的顺序遍历每个桶内全部数据,自然就是有序的了。

计数排序相当于每个数字是一个桶,一个桶只会有"一种多个"数字,所以计数排序可以认为是桶排序的一个特例

桶排序体现了一种数据分布的思想,合理的划分方式,让数据分在多个桶内,实现了数据分片,每个桶内的排序工作量就大大减轻了,很容易发现,它适合大数据分布式运行,可以作为解决一些大数据问题的方法之一,适用场景的典型特征就是原始数据有明显适合的划分方式,如15亿中国人按年龄排序、海量数据中第k大/中位数之类,代码中包括中位数求法。


代码:

#include <iostream>
#include <functional>
#include <random>
#include <vector>
#include <algorithm>


int cutf (int max, int cur) {
    return cur/max;
}
using CutFunc = std::function<int (int, int)>;


class BucketSort {
    int *rawdata;
    int size;
    int max;
    int bucket_num;
    std::vector<int> *buckets;
    std::function<int (int, int)> &cutfunc;


public:
    BucketSort (int *_data, int _size, int _bucket_num, int _max, CutFunc &_cutfunc):size(_size), bucket_num(_bucket_num), max(_max), rawdata(_data), cutfunc(_cutfunc) {
        buckets = new std::vector<int>[bucket_num];
    }


    ~BucketSort () {
        delete []buckets;
    }


    void bucketsort () {
        for (int i = 0; i < size; i++) {
            int idx = cutfunc(max, rawdata[i]);
            buckets[idx].push_back(rawdata[i]);
        }


        for (int i = 0; i < bucket_num; i++) {
            std::sort(buckets[i].begin(), buckets[i].end());
        }


        for (int i = 0; i < bucket_num; i++) {
            for (int j = 0; j < buckets[i].size(); j++) {
                std::cout << buckets[i][j] << "\t";
            }
        }
        std::cout << std::endl;
    }


    int Get_Median () {
        int half = size/2, sum = 0;
        for (int i = 0; i < bucket_num; i++) {
            int cur = buckets[i].size();
            if (sum + cur < half) {
                sum += cur;
            } else if (sum + cur == half) {
                return buckets[i][buckets[i].size() - 1];
            } else {
                return buckets[i][sum + cur - half];
            }
        }
    }
};


int main () {
    std::random_device rd;
    int data[100];
    int max = 10000;
    for (int i = 0; i < 100; i++) {
        int cur = rd() % max;
        std::cout << cur << ",\t";
        data[i] = cur;
    }
    std::cout << std::endl;


    int bucket_num = 100;
    CutFunc cutfunc = &cutf;
    BucketSort bs(data, sizeof(data)/sizeof(data[0]), bucket_num, max, cutfunc);
    bs.bucketsort();
    std::cout << bs.Get_Median() << std::endl;
    return 0;
}



基数排序:


思路:基数排序本质上有比桶排序和计数排序都更多的东西,类似于桶排序的是,也用自定义的划分方式,而基数排序更是不断划分数据,所谓的”基数“就是一直根据某种方式划分数据的思想。比如,在海量数据中寻找中位数,基数排序有更为开放的思路:

海量数据寻找中位数,假设是int32的非负整数即0到2^32 - 1(负数也可以搞,稍加修改即可(统一加32768),基数排序不是计数排序):

桶排序:根据自定义的区间创建若干桶,然后真的把海量数据每个数据放入对应区间的桶中且同时纷纷排序,然后寻找中位的桶及其里边对应位置处数据

基数排序:1、2^16 = 65536,上限是2^32即65536 * 65536,那就创建65536个桶,然后每个数除以65536,除数肯定位于0-65535中,每个桶只做累加;O(N)

    2、统计每个桶内累加计数之和,像桶排序一样找到中位数位于哪个桶中,比如是32777号桶,同时要记下到32776号桶已经有多少个数据了,比如全部数据量是100 * 1000 * 1000为一亿,假设到32776号桶已经有9990万了,这样问题就转化为,确定会落在32777号桶里的按顺序的第10万名是谁;O(65536)

    3、遍历全量数据,把会落在32777号桶的数据放入32777号桶,方式同1,然后排序并找到第10万名是谁,这个就是中位数了;O(N + klog(k))或O(N + k),k是32777号桶容量,用原地比较排序就是O(N + klog(k))要不就是O(N + k)

总计:相当于O(N * 2)


相比桶排序,免去了大量的桶空间占用,因为无需放入数据,只需要65536容量数组累加,而且免去了大量的每个桶排序,时间复杂度也更快一些。


下面代码是简单的对数字的基数排序,看起来更像是计数排序和桶排序的结合,核心思想是:按10进制数字从低位到高位为标准,不断的排序,即依次按个位、十位、百位、......位数不足的会是0,比如38,135,1111这3个数,首先按个位排序是1111、135、38,然后更新入桶里,再按十位排序是1111、135、38更新入桶,再按百位是38、1111、135更新入桶,最后是千位是38、135、1111,O(N)时间复杂度,O(N + 10)空间复杂度。

上面的按个位、十位这样的方式,对于基数排序的"基数"来说,每个数字10进制的依次的低位就是基数,还可以有其他自定义的各种”基数“去排序,随便举例如还可以按高位余数排序,如字符串的自定义排序规则、甚至复合结构比如典型一些面试题”全校学生的多个科目成绩表,按不同科目不同优先级排序“等等;也就是基数排序其实更多是一种”根据多维实现自定义排序“的思想吧,下面代码不重要,重在理解”基数“内涵。


代码:

#include <iostream>
#include <random>
#include <memory.h>

int get_bit_num (int max) {
    int cnt = 0;
    while (max) {
        max /= 10;
        if (max) {
            ++cnt;
        }
    }

    return cnt;
}

void radixsort (int *data, int size, int max) {
    int times = get_bit_num(max), base = 1;
    int range[10] = {0};
    int res[size];
    
    for (int i = 0; i < times; i++) {
        memset(range, 0, sizeof(range));
        
        for (int i = 0; i < size; i++) {
            int remander = (data[i]/base) % 10;
            ++range[remander];
        }

        for (int i = 1; i < 10; i++) {
            range[i] += range[i - 1];
        }

        for (int i = size - 1; i >= 0; i--) {
            res[range[(data[i]/base) % 10] - 1] = data[i];
            --range[(data[i]/base) % 10];
        }

        for (int i = 0; i < size; i++) {
            data[i] = res[i];
        }

        base *= 10;
    }

    for (int i = 0; i < size; i++) {
        std::cout << data[i] << ",\t";
    }
    std::cout << std::endl;
}

int main () {
    int max = 10000;
    std::random_device rd;
    int data[2000];
    for (int i = 0; i < 2000; i++) {
        int cur = rd() % max;
        std::cout << cur << ",\t";
        data[i] = cur;
    }
    std::cout << std::endl;

    radixsort(data, sizeof(data)/sizeof(data[0]), max);
    return 0;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值