基数排序
基数排序是桶排序的一种推广, 它所考虑的待排记录包含不止一个关键字。
例如对一副牌的整理,可将每张牌看作一个记录,包含两个关键字:花色、面值。
一副理顺的牌是按如下顺序排放的:
♣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;
}
}