与之前的那些比较排序不同,分配排序在排序过程无须比较关键字,而是通过"分配"和"收集"过程来实现排序。它们的时间复杂度可达到线性阶:O(n)。常见的分配排序有计数排序(Counting Sort),基数排序(Radix Sort),桶排序(Bucket Sort),美国旗帜排序(American Flag Sort),珠排序(Bead Sort),爆炸排序(Burst Sort),鸽巢排序(Pigeonhole Sort),相邻图排序(Proxmap Sort),闪电排序(Flash Sort)。下面介绍前三种:
(一)计数排序
最差时间复杂度:O(n+k)
最优时间复杂度:O(n+k)
平均时间复杂度:O(n+k)
最差空间复杂度:O(n+k)
稳定性:稳定
计数排序(Counting Sort),使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数,然后根据数组C来将A中的元素排到正确的位置。当输入的元素是n个0到k之间的整数时,它的运行时间是O(n+k)。技术排序不是比较排序,排序数独快于任何比较排序。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
计数排序的一般步骤如下:
1.找出待排序的数组中最大和最小的元素max和min,申请大小与A数组相同的B数组,和大小为(max-min+1)的C数组;
2.统计数组中每个值为i的元素出现的次数,存入数组C的第(i-min)项;
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组B的第(C(i-min)-1)项,每放一个元素就将C(i-min)减去1;
5.将排序的B数组拷贝回A数组。
算法示意图:
实现代码:
void CountingSort(int *a, int len)
{
int i;
int max = -10000000;
int min = 10000000;
// 查找数列最大最小值
for (i=0; i<len-1; i=i+2)
{
if (a[i]>a[i+1])
{
if (a[i]>max)
max = a[i];
if (a[i+1]<min)
min = a[i+1];
} else {
if (a[i+1]>max)
max = a[i+1];
if (a[i]<min)
min = a[i];
}
}
if (i == len-1)
{
if (a[i]>max)
max = a[i];
else if(a[i]<min)
min = a[i];
}
// 排序
int lenC = max-min+1;
int *B = new int[len];
int *C = new int[lenC];
for (i=0; i<lenC; i++)
C[i] = 0;
for (i=0; i<len; i++)
C[a[i]-min]++;
for (i=1; i<lenC; i++)
C[i] += C[i-1];
for (i=len-1; i>=0; i--)
{
B[C[a[i]-min]-1] = a[i];
C[a[i]-min]--;
}
for (i=0; i<len; i++)
a[i] = B[i];
delete []B;
delete []C;
}
(二)基数排序
最差时间复杂度:O(nk)
稳定性:稳定
基数排序(Radix Sort),一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较,首先按最低有效位数字进行排序。要保证基数排序是正确的,就必须保证按位排序是稳定的。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
算法示意图:
实现代码:
void RadixSort(int *a, int len)
{
int i, *B, max=a[0], exp=1;
B = new int[len];
for (i=0; i<len; i++){
if (a[i]>max)
max = a[i];
}
while (max/exp>0)
{
// 中间稳定排序使用计数排序
int C[10] = {0};
for (i=0; i<len; i++)
C[a[i]/exp%10]++;
for (i=1; i<10; i++)
C[i] += C[i-1];
for (i=len-1; i>=0; i--)
B[--C[a[i]/exp%10]] = a[i];
for (i=0; i<len; i++)
a[i] = B[i];
exp *= 10;
}
}
(三)桶排序
最差时间复杂度:O(n^2)
平均时间复杂度: O(n+k)
最差空间复杂度: O(n*k)
稳定性:稳定
桶排序(Bucket Sort),其原理是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。
桶排序的一般步骤:
1.设置一个定量的阵列当作空桶子。
2.寻访序列,并且把项目一个一个放到对应的桶子去。
3.对每个不是空的桶子进行排序。
4.从不是空的桶子里把项目再放回原来的序列中。
例如:针对记录的数据大小在[0,1)内,则把[0,1)划分为n个大小相同的子区间,每一子区间是一个桶。然后将n个记录分配到各个桶中。因为关键字序列是均匀分布在[0,1)上的,所以一般不会有很多个记录落入同一个桶中。由于同一桶中的记录其关键字不尽相同,所以必须采用关键字比较的排序方法(通常用插入排序)对各个桶进行排序,然后依次将各非空桶中的记录连接(收集)起来即可。
一般情况下每个桶中存放多少个关键字相同的记录是无法预料的,故桶的类型应设计成链表为宜,而为了加快算法效率,再往桶中放入记录时就同时进行插入排序。
算法示意图:
实现代码:
struct node{
double data;
node *next;
};
int key(double a)
{
return (int)a*10;
}
void BucketSort(double *a, int len)
{
node buckets[10];
node *b = new node[len];
for (int i=0; i<10; i++){
buckets[i].data = 0;
buckets[i].next = NULL;
}
for (int i=0; i<len; i++){
b[i].data = a[i];
b[i].next = NULL;
}
for (int i=0; i<len; i++)
{
node *p = buckets[key(b[i].data)].next;
node *q = &buckets[key(b[i].data)];
while(p!=NULL)
{
if(p->data<=b[i].data){
q = p;
p = p->next;
}else{
break;
}
}
q->next = &b[i];
b[i].next = p;
}
for (int i=0, j=0; j<10; j++)
{
node *p = buckets[j].next;
while (p!=NULL)
{
a[i++] = p->data;
p = p->next;
}
}
delete []b;
}