排序系列(九)基数排序

120 阅读3分钟

基数排序
基数排序是桶排序的一种推广, 它所考虑的待排记录包含不止一个关键字。
例如对一副牌的整理,可将每张牌看作一个记录,包含两个关键字:花色、面值。
一副理顺的牌是按如下顺序排放的:
♣2...A♣,◆2..A◆,♥2..A♥,♠2..A♠
可见一个有序列是先按花色划分为四大块,每一块中又再按面值大小排序。
这时“花色”就是一张牌的“最主位关键字”,而“面值"是“最次位关键字”。
对于一般有K个关键字的情况,基数排序通常有两种方法:主位优先法(Most SignificantDig-it Sort, 简称MSD)
和次位优先法( Least Significant Digit Sort,简称LSD)。
仍以整理扑克牌为例,顾名思义,所谓主位优先法,是先为最主位关键字(花色)建立桶,
将牌按花色分别装进4个桶里;然后对每个桶中的牌,再按其次位关键字(面值)进行排序,
最后将4个桶中的牌收集,顺序叠放在一起。
而次位优先法,是先为最次位关键字建立桶,即按面值建立13个桶,将牌按面值分别放于13个桶中;
然后将所有桶中的牌收集,顺序叠放在一起;
再为主位关键字花色建立4个桶,顺序将每张牌放人对应的花色桶中,则4个花色桶中的牌必是有序的,
最后只要将它们收集,顺序叠放即可。
从上述例子可见,两种方法具有不同的特点。
主位优先法基本上是分治法的思路,将序列分割成子列后,分别排序再合并结果。
而次位优先法是将“排序"过程分解成了“分配”和“收集”这两个相对简单的步骤,并不需要分割子列排序,故一般情况下次位优先法的效率更高一些。

基数分解
从上面可以看到,基数排序主要是对有多关键字的对象进行排序。
其实可以将单个整型关键字按某种基数分解为多关键字,再进行排序。这也是“基数排序”名称的由来。
例如826可以根据基数10分解为8、2、6这三个关键字,其中8是最主位关键字,6是最次位关键字;
还可以根据基数16分解为3.3、A这3个关键字,其中第一个3是最主位关键字,A是最次位关键字。
典型问题是给定N个记录,每个记录的关键字为一个整数,其取值范围在0到M之间。若M比N大很多(例如M=N*),这时桶排序需要M个桶,会造成巨大的空间浪费;
而以R为基数对关键字进行分解后则只需要R个桶就可以了。

算法实现-次位优先法(C#):

 

// 关键词最大个数
private static readonly int MaxDigit = 3;
// 分解基数  基数和关键词最大个数需根据实际情况选取
private static readonly int Radix = 10;
// 桶节点
private class BucketNode
{
    public int Key { get; set; }
    public BucketNode Next { get; set; }
}
// 桶头节点
private class HeadBucketNode
{
    public BucketNode Head { get; set; }
    public BucketNode Tail { get; set; }
}
// 获取元素指定位序关键词 位序从1开始 1=最次位关键字,maxDigit-1=最主位关键字
private static int GetNumberDigit(int key, int digit)
{
    int result = 0;
    for (int i = 1; i <= digit; i++)
    {
        result = key % Radix;
        key /= 10;
    }
    return result;
}
// 次位排序
public static void LSDRadixSort(int[] arr)
{
    // 初始化变量
    // 分解关键词
    int subKey;
    BucketNode head = null;
    BucketNode tmp = null;
    BucketNode p = null;
    HeadBucketNode[] buckets;
    // 初始化桶
    buckets = new HeadBucketNode[Radix];
    for (int i = 0; i < Radix; i++)
    {
        buckets[i] = new HeadBucketNode()
        {
            Head = null,
            Tail = null
        };
    }
    // 将待排序数组倒序/顺序进入链表
    // N
    for (int i = 0; i < arr.Length; i++)
    {
        tmp = new BucketNode();
        tmp.Key = arr[i];
        tmp.Next = head;
        head = tmp;
    }
    // 按关键词位序进行排序  从最次位关键字到最主位关键字,顺序不可逆
    for (int keyIndex = 1; keyIndex <= MaxDigit; keyIndex++)
    {
        // 将元素分配到各个桶中
        // 根据位序分解关键词
        p = head;
        // N
        while (p != null)
        {
            subKey = GetNumberDigit(p.Key, keyIndex);
            tmp = p;
            p = p.Next;
            tmp.Next = null;
            if (buckets[subKey].Head == null)
            {
                buckets[subKey].Head = tmp;
                buckets[subKey].Tail = tmp;
            }
            else
            {
                buckets[subKey].Tail.Next = tmp;
                buckets[subKey].Tail = tmp;
            }
        }
        //  M
        // 将元素收集入链表
        head = null;
        for (subKey = Radix - 1; subKey >= 0; subKey--)
        {
            if (buckets[subKey].Head != null)
            {
                buckets[subKey].Tail.Next = head;
                head = buckets[subKey].Head;
                buckets[subKey].Head = null;
                buckets[subKey].Tail = null;
            }
        }
    }
    // 将已排序好的链表复制回数组
    for (int i = 0; i < arr.Length; i++)
    {
        arr[i] = head.Key;
        head = head.Next;
    }
}

对N个关键字用R个桶进行基数排序时,其时间复杂度为0(D(N+R)),其中D为分配收集的趟数,\ 也就是关键字按基数分解后的位数。当记录的个数N与桶的个数基本是同一数量级时,基数排序可以达到线性复杂度。
但注意到由于链表指针操作的引入,这个0(N)的常数项可能会超过logN,从而实际效果也未必比前面讲过的几种算法要好很多。
另一方面,基数排序用链表实现的好处是不需要将记录进行物理移动,对于大型记录的排序是有利的,代价是需要O(N)额外空间存放指针。
总之,基数排序的效率与基数的选择密切相关,而基数的选择需要综合考虑待排记录的规模和关键字的取值范围。

算法实现-主位优先法(C#):

 

// 基数排序-主位优先法
public static void MSDRadixSort(int[] arr)
{
    // 初始化桶
    int mainKey;
    int subKey;
    BucketNode tmp;
    BucketNode headTmp;
    HeadBucketNode[] buckets = new HeadBucketNode[Radix];
    HeadBucketNode[] subBuckets = new HeadBucketNode[Radix];
    for (int i = 0; i < buckets.Length && i < subBuckets.Length; i++)
    {
        buckets[i] = new HeadBucketNode()
        {
            Head = null,
            Tail = null
        };
        subBuckets[i] = new HeadBucketNode()
        {
            Head = null,
            Tail = null
        };
    }
    // 按主关键词进行桶的分配
    for (int i = 0; i < arr.Length; i++)
    {
        mainKey = GetNumberDigit(arr[i], MaxDigit);
        tmp = new BucketNode();
        tmp.Key = arr[i];
        tmp.Next = null;
        if (buckets[mainKey].Head == null)
        {
            buckets[mainKey].Head = tmp;
            buckets[mainKey].Tail = tmp;
        }
        else
        {
            buckets[mainKey].Tail.Next = tmp;
            buckets[mainKey].Tail = tmp;
        }
    }
    // 对桶内关键词进行排序
    // 处理所有次位关键词
    for (int d = 1; d < MaxDigit; d++)
    {
        for (int i = 0; i < buckets.Length; i++)
        {
            headTmp = buckets[i].Head;
            while (headTmp != null)
            {
                // 分配
                subKey = GetNumberDigit(headTmp.Key, d);
                tmp = new BucketNode();
                tmp.Key = headTmp.Key;
                tmp.Next = null;
                if (subBuckets[subKey].Head == null)
                {
                    subBuckets[subKey].Head = tmp;
                    subBuckets[subKey].Tail = tmp;
                }
                else
                {
                    subBuckets[subKey].Tail.Next = tmp;
                    subBuckets[subKey].Tail = tmp;
                }
                headTmp = headTmp.Next;
            }
            // 收集
            buckets[i].Head = null;
            buckets[i].Tail = null;
            for (int j = subBuckets.Length - 1; j >= 0; j--)
            {
                if (subBuckets[j].Head != null)
                {
                    subBuckets[j].Tail.Next = buckets[i].Head;
                    buckets[i].Head = subBuckets[j].Head;
                    if (buckets[i].Tail == null)
                        buckets[i].Tail = subBuckets[j].Tail;
                    subBuckets[j].Head = null;
                    subBuckets[j].Tail = null;
                }
            }
            PrintBucketLinkList(buckets[i].Head);
        }
    }
    // 收集
    headTmp = null;
    for (int i = buckets.Length - 1; i >= 0; i--)
    {
        if (buckets[i].Head != null)
        {
            buckets[i].Tail.Next = headTmp;
            headTmp = buckets[i].Head;
            buckets[i].Head = null;
            buckets[i].Tail = null;
        }
    }
    // 将排序后的序列复制回数组
    for (int i = 0; i < arr.Length; i++)
    {
        arr[i] = headTmp.Key;
        headTmp = headTmp.Next;
    }
}