桶排序
在介绍基数排序之前,我们先看看桶排序的原理。
假设我们有N个学生,他们的成绩是0到100之间的整数(于是有M = 101个不同的成绩值)。如何在线性时间内将学生按成绩排序?
在这个例子中我们看到数据的特殊性,学生可以有很多,但是种类就只有101种,我们建立101个桶,每个桶一个成绩值,桶中的存放的是链表,相同成绩的学生在对应的桶中构成链表,扫描每个学生的成绩,依次将其插入到对应的桶中。
每个桶被初始化为一个空链表的头指针,也就有了101个空链表的头指针,初始时有101个空链表,也就有101个桶。现在假设扫描到一个同学的成绩为88,找到88对应的桶,将该元素插如到对应的表头里。
输出时,按照桶的位置,如果桶不为空,将桶中的元素依次输出,再输出下一个桶的元素。
伪码描述:
void Bucket_Sort(ElementType A[], int N)
{
count[]初始化;
while (读入1个学生成绩grade)
将该生插入count[grade]链表;
for (i = 0; i<M; i++) {
if (count[i])
输出整个count[i]链表;
}
}
时间复杂度:O(N+M)
每个元素扫描一遍,每个桶的中链表的头节点扫描一遍。
当M<<N时,可以看成是一种线性的算法。
基数排序
再看一个例子,假设我们有N = 10 个整数,每个整数的值在0到999之间(于是有 M = 1000 个不同的值)。还有可能在线性时间内排序吗?
输入序列: 64, 8, 216, 512, 27, 729, 0, 1, 343, 125
这里的基数指的是进制,2进制基数等于2,8进制基数等于8,现在是10进制的数,基数等于10。
用“次位优先”(Least Significant Digit)
我们采取的策略是次位优先,比如512,个位2为最次的位置,百位5为主位,先从个位开始排序,再对十位,百位排序。
基数是10,我们就建立10个桶,先按照个位对所有元素排序,再个位排序的基础上按照十位对所有元素排序,最后在前面排序的基础上按照百位排序。
设元素个数为N,整数进制为B,LSD的趟数为P,则最坏时间复杂度是:T=O(P(N+B))
一共有P趟,在每一每一趟趟中要对B个桶,及所有元素扫描一遍,所有每一趟访问元素个数为N+B。
次位优先完整c++代码
//基数排序 - 次位优先
#include<iostream>
using namespace std;
typedef int ElementType;
#define MaxDigit 4 //假设元素最多有MaxDigit个关键字
#define Radix 10 //基数全是同样的Radix
//桶元素结点
typedef struct Node *PtrToNode;
struct Node {
int key;
PtrToNode next;
};
//桶头结点
typedef struct HeadNode {
PtrToNode head, tail;
}Bucket[Radix];
//typedef struct HeadNode Bucket[Radix];
int GetDigit(int X, int D)//获得当前元素的当前位数字
{//默认次位D=1, 主位D<=MaxDigit
int d, i;
for (i = 1; i <= D; i++)
{
d = X % Radix;
X /= Radix;
}
return d;
}
//基数排序 - 次位优先
void LSDRadixSort(ElementType A[], int N)
{
int D, Di, i;
Bucket B;
PtrToNode tmp, p, List = NULL;
for (i = 0; i<Radix; i++) //初始化每个桶为空链表
B[i].head = B[i].tail = NULL;
for (i = 0; i<N; i++) //将原始序列逆序存入初始链表List
{
tmp = (PtrToNode)malloc(sizeof(struct Node));
tmp->key = A[i];
tmp->next = List;
List = tmp;
}
//下面开始排序
for (D = 1; D <= MaxDigit; D++)//对数据的每一位循环处理
{
//下面是分配的过程
p = List;
while (p)
{
Di = GetDigit(p->key, D); //获得当前元素的当前位数字
tmp = p; p = p->next; //从List中摘除
tmp->next = NULL;
if (B[Di].head == NULL)
B[Di].head = B[Di].tail = tmp;
else //插入B[Di]号桶尾
{
B[Di].tail->next = tmp;
B[Di].tail = tmp;
}
}
//下面是收集的过程
List = NULL;
//将每个桶的元素顺序收集入List,
//因为每次都将桶的元素插入List表头,所以从关键字大的开始
for (Di = Radix - 1; Di >= 0; Di--)
{
if (B[Di].head)//如果桶不为空,整桶插入List表头
{
B[Di].tail->next = List;
List = B[Di].head;
B[Di].head = B[Di].tail = NULL; //清空桶
}
}
}
//将List倒入A[]并释放空间,这时的List是已经排好序的
for (i = 0; i<N; i++)
{
tmp = List;
List = List->next;
A[i] = tmp->key;
free(tmp);
}
}
int main()
{
int a[] = { 524, 346, 91, 258, 439, 613, 257, 10 };
int len = sizeof(a) / sizeof(a[0]);
LSDRadixSort(a, len);
for (int i = 0; i < len; i++)
cout << a[i] << " ";
return 0;
}
主位优先完整c++代码
//基数排序 - 主位优先
#include<iostream>
using namespace std;
typedef int ElementType;
#define MaxDigit 4 //假设元素最多有MaxDigit个关键字
#define Radix 10 //假设基数全是同样的Radix
typedef struct Node *PtrToNode;//桶元素结点
struct Node
{
int key;
PtrToNode next;
};
typedef struct HeadNode { //桶头结点
PtrToNode head, tail;
}Bucket[Radix];
int GetDigit(int X, int D) //默认次位D=1, 主位D<=MaxDigit
{
int d, i;
for (i = 1; i <= D; i++)
{
d = X % Radix;
X /= Radix;
}
return d;
}
//核心递归函数: 对A[L]...A[R]的第D位数进行排序
void MSD(ElementType A[], int L, int R, int D)
{
int Di, i, j;
Bucket B;
PtrToNode tmp, p, List = NULL;
if (D == 0) return; //递归终止条件
for (i = 0; i<Radix; i++) //初始化每个桶为空链表
B[i].head = B[i].tail = NULL;
for (i = L; i <= R; i++) //将原始序列逆序存入初始链表List
{
tmp = (PtrToNode)malloc(sizeof(struct Node));
tmp->key = A[i];
tmp->next = List;
List = tmp;
}
//下面是分配的过程
p = List;
while (p)
{
Di = GetDigit(p->key, D); //获得当前元素的当前位数字
tmp = p; p = p->next; //从List中摘除
//插入B[Di]号桶
if (B[Di].head == NULL)
B[Di].tail = tmp;
tmp->next = B[Di].head;
B[Di].head = tmp;
}
//下面是收集的过程
i = j = L; //i, j记录当前要处理的A[]的左右端下标
for (Di = 0; Di<Radix; Di++) //对于每个桶
{
if (B[Di].head) //将非空的桶整桶倒入A[], 递归排序
{
p = B[Di].head;
while (p)
{
tmp = p;
p = p->next;
A[j++] = tmp->key;
free(tmp);
}
MSD(A, i, j - 1, D - 1);//递归对该桶数据排序, 位数减1,j指向桶最后一个元素的下一个
i = j; //为下一个桶对应的A[]左端
}
}
}
void MSDRadixSort(ElementType A[], int N)//统一接口
{
MSD(A, 0, N - 1, MaxDigit);
}
int main()
{
int a[] = { 524, 346, 91, 258, 439, 613, 257, 10 };
int len = sizeof(a) / sizeof(a[0]);
MSDRadixSort(a, len);
for (int i = 0; i < len; i++)
cout << a[i] << " ";
return 0;
}
多关键字的排序
基数排序可以用来处理多关键字的排序。下面来看个例子:
一副扑克牌是按2种关键字排序的,花色和面值。
花色为主关键字,面值为次关键字。
在排序的时候,次关键字对应低位,主关键字对应高位。
除了次位优先排序,还有主位优先排序。
用“主位优先” ( Most Significant Digit ) 排序:为花色建4个桶,在每个桶内调用排序算法分别排序,最后合并结果。
这种方法还需要在每个桶里面用排序算法。
下面来看用“次位优先” ( Least Significant Digit ) 排序:为面值建13个桶,将元素扫描一遍插入相应的桶中,再为花色建4个桶,从面值桶的第一个开始扫面,依次将每种花色放入对应的桶中,最后合并就是排序结果。
主位优先(MSD) 和次位优先(LSD)各有各的适用场合,不一定谁比谁好。