基数排序

基数排序(Radix Sort)属于分配式排序算法的一种,它是将待排序序列的元素切分为许多个部分作为排序键,对序列的每一趟排序都会更换不同的键(由低位到高位或是由高位到低位)进行计数排序。这种排序思想要求每一趟排序都是稳定的,即相同键值的相对位置不会发生改变,而计数排序可以满足这个要求。

低位优先

低位优先(Least Significant Digital,LSD)的基数排序主要应用于元素都是定长的序列——电话号码、银行卡号、身份证号、IP地址等。假设字符串的长度为W,LSD基数排序的执行过程就是从右向左以每个位置的字符作为键,用计数排序将字符串排序W遍。示例如下:

初始序列(W=3) d=2 d=1 d=0 输出
QXH SSD D ROM O HXY H HXY
SSD QXH H SSD S QXH Q QXH
HXY ROM M QXH X ROM R ROM
ROM HXY Y HXY X SSD S SSD

稳定性是基数排序是否有效的关键,这意味着我们可以采用具有稳定性的基于比较的排序算法,如插入排序、归并排序等(这对于优化基数排序对巨型字符串的排序过程有着重要作用)。 

const int k = 256; //元素范围

void RadixSort_LSD(vector &str_vec)
{
    for (int d = str_vec.front().size()-1; d >= 0; --d)
    {
        //对不同d值采用计数排序
        vector cnt(k);

        for (const string &s : str_vec) //统计频率
        {
            ++cnt[s[d]];
        }

        for (int i = 1; i < cnt.size(); ++i) //将频率转化为索引
        {
            cnt[i] += cnt[i-1];
        }

        vector aux = str_vec;
        for (int i = 0; i < str_vec.size(); ++i) //分类并回写
        {
            str_vec[--cnt[aux[i][d]]] = aux[i];
        }
    }
}

老式的卡片打孔排序机用的就是低位优先的基数排序法,因为打孔机一次只能看到一列,这也是基数排序的起源。那时对于长度不等的元素,程序员将高位补0然后再排序。而对于不定长元素的序列,我们也只需要稍加改造就可以使用低位优先的基数排序了。从理论上说,低位优先的基数排序意义重大,因为它是一种适用于一般应用的线性时间排序算法。假设元素大小为W,无论元素个数N有多大,它都只遍历W次数据。

高位优先

低位优先的基数排序只能处理定长字符串的排序,要实现一个通用的字符串排序算法,应该考虑从左向右遍历所有字符,即采用高位优先(Most Significant Digital, MSD)的基数排序。示例如下:

由上面示例可以看出,高位优先的基数排序采用了递归切分的思想,每一趟计数排序完成后,将元素按照当前键分类,在下一趟计数排序时会对之前的每一组分类分别进行计数排序。

const int k = 256; //元素范围

void RadixSort_MSD(vector &str_vec, vector &aux, int start, int end, int d)
{
    if (start <= end)
    {
        return ;
    }

    int i = 0;
    vector cnt(k+1, 0);
    cnt.back() = str_vec.size()-1; //控制边界

    for (i = start; i <= end; ++i) //统计频率
    {
        ++cnt[str_vec[i][d]];
    }

    for (i = 1; i < k; ++i) //将频率转化为索引
    {
        cnt[i] += cnt[i-1];
    }

    for (i = start; i <= end; ++i) //分类并回写
    {
        str_vec[--cnt[aux[i][d]] + start] = aux[i]; //注意cnt[]记录的是子序列的相对位置,需要转换为整个序列的绝对位置
    }

    for (i = 0; i < k; ++i) //对分类结果,即对每一个键进行基数排序
    {
        RadixSort_MSD(str_vec, aux, start+cnt[i]+1, start+cnt[i+1], d+1);
    }
}

 高位优先的基数排序的切分是非常有效的,但由于基数排序是以空间换时间的算法,所以如果对于巨型字符串,切分到后期就会产生大量的用于统计频率的向量cnt[],因为每一次递归都需要重新创建统计频率的向量——相较于同样受小型子序列影响排序性能的归并排序和快速排序,高位优先基数排序更为明显。解决方案通常是对小型子序列采用插入排序。此外高位优先基数排序也同样受序列元素特点的影响,比如含有大量重复键的元素,对应的优化方案与快速排序的优化类似。

 

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