桶排序
如果已知N个关键字的取值范围是在0到M-1之间,并且M比N小很多,这种情况下就用桶排序,其原理就是为关键字的每个可能取值建立一个“桶”(Bucket),也就是建立M个桶;在扫描N个关键字时,每遇到一个关键字,就把它丢到相应的桶里去,扫描完了按桶的顺序对数据收集一遍就自然有序了。所以桶的排序效率比一般排序算法的效率要高。
这里呢有一个数组A[20] = {5,6,9,6,3,4,7,3,0,1,2,5,9,6,3,0,5,9,9,7},就好比幼儿园小班有20个小朋友,有一次算术考试,满分10分(0分是因为这两个难兄难弟完美的避开了每个正确答案,哈哈哈),上面数组中的每个元素就代表了每个小朋友考的分数。好吧,现在童年悲剧来了,要对成绩进行排序了o(╥﹏╥)o
- 分析小朋友们的分数,可以发现分数均在0-9之间,因此呢我们就可以准备10个桶,编号分别为0-9,也就是申请一个大小为10的数组Bucket[10]。
- 在排序之前,先对每个桶初始化,即Bucket[0]-Bucket[9]均初始化为0,表示这些分数还没有小朋友得过。
- 顺序扫描A[20]这个数组,每遇到一个小朋友的分数,就找该分数对应哪个桶,找到了就把分数丢到桶里去,然后桶里面的数+1,每丢一次就+1,因为所有分数都在0-9的范围内且不存在小数,因此最后所有的分数都能被丢到相应的桶里,也就是下面那幅图。
- 都丢完后,每个桶里的数字是几,就代表这个桶对应的分数出现了几次。按照桶从大到小的顺序,将出现的分数打印出来,出现几次打印几次就行了。
这里笔者分别使用数组和链表来进行表示
#include <stdio.h>
#include <stdlib.h>
typedef int ElemenetType;
/*
举例:待排序列为A={5,6,9,6,3,4,7,3,0,1,2,5,9,6,3,0,5,9,9,7},均属于(0,10)的数据
因此安排10个桶,分别为0-9
1.数组简单表示
2.链表详细表示
*/
/*1.数组简单表示*/
void BucketSort_array(ElemenetType A[], int N)
//统一函数接口
{
ElemenetType book[10] = {0};
for (int i = 0; i < N; i++){
book[A[i]]++;//在桶中丢入一个标记
}
for (int j = 9; j >= 0; j--){//依次判断编号9-0的桶
for ( int k = 0; book[j] != 0 && k < book[j]; k++)
printf("%d ", j);//出现了几次就把桶的编号打印几次
}
printf("\n");
}
/*2.链表详细表示*/
#define index 10 //桶的个数为10
//定义桶元素结点
typedef struct Node *PNode;
struct Node{
ElemenetType Data;
PNode next;
};
//定义桶头结点
struct HNode{
PNode head, last;
};
typedef struct HNode Bucket[index];
void BucketSort_list(ElemenetType A[], int N)
{
//同样是10个桶,编号为0-9,因此index = 10
Bucket B;
for (int i = 0; i < index; i++)
B[i].head = B[i].last = NULL;//初始化桶头结点
for (int j = 0; j < N; j++){
PNode temp = (PNode)malloc(sizeof(struct Node));
if (B[A[j]].head == NULL){
B[A[j]].last = (PNode)malloc(sizeof(struct Node));
B[A[j]].head = B[A[j]].last;
}
temp->Data = A[j];
temp->next = NULL;
//将A[j]丢入相应的桶B[A[j]]内
B[A[j]].last->next = temp;
B[A[j]].last = temp;
}
for (int j = index - 1; j >= 0; j--){
if (B[j].head != NULL)
B[j].head = B[j].head->next;
}
for (int j = index - 1; j >= 0; j--){
while (B[j].head != NULL){
printf("%d ", B[j].head->Data);
B[j].head = B[j].head->next;
}
}
printf("\n");
}
int main(int argc, char const *argv[])
{
int N;
ElemenetType A[] = {5,6,9,6,3,4,7,3,0,1,2,
5,9,6,3,0,5,9,9,7};
N = sizeof(A) / sizeof(int);
BucketSort_array(A, N);//数组表示
BucketSort_list(A, N);//链表表示
system("pause");
return 0;
}
下面再举个进阶版本的栗子。
我先用进阶版本的语言描述一下这个进阶版本的桶排序思想o( ̄▽ ̄)d good:
假设有一组长度为N的待排关键字序列K[1…n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]…B[M]中的全部内容即是一个有序序列。在bindex=f(key)中,bindex为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1<k2,那么f(k1)<=f(k2)。也就是说B[i]中的最小数据都要大于B[i-1]中最大数据。很显然,映射函数的确定与数据本身的特点有很大的关系。
基本流程(。・ω・。)
- 分析数据,建立映射函数,搬来一堆Buckets;
- 遍历原始数组,并将数据放入各自的Bucket中;
- 对非空的Buckets进行排序(可选择效率高的比较排序);
- 按照顺序遍历这些Buckets并放回到原始数组中即可构成排序后的数组。
好吃的栗子与图示ლ(´ڡ`ლ)
假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行比较排序后得到如图所示。只要顺序输出每个B[i]中的数据就可以得到有序序列了。
可食用的参考代码(๑•̀ㅂ•́)و✧:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
/*
---------------进阶版本的桶排序---------------
- 待排序列A[] = {49, 34, 31, 79, 73, 27, 49
, 56, 87, 35, 57, 10, 78, 67, 33, 98, 63, 47,
89, 46, 76, 23, 19, 2, 69, 3, 0, 1, 89, 14,};
- 搬来10个Buckets
- 确定映射函数f(k)=k/10
- 此处选择插入排序对桶内元素进行排序
- 从大到小进行排序
- --------------------------------------------
*/
#define BucketSize 10 //桶的个数
#define INTERVAL 10 //映射函数中的参数
//桶元素结点
typedef struct Node *PNode;
struct Node{
ElementType Data;
PNode next;
};
//桶头结点
struct HNode{
PNode head, last;
};
typedef struct HNode Bucket[BucketSize];
PNode InsertionSort(PNode bfr_list);
void BucketSort_plus(ElementType A[], int N);
int GetPosition(int X);
void Print(ElementType A[], int N);
void PrintBuckets(PNode bfr_list);
int GetPosition(int X)
//获得元素对应的桶的下标
{
int index;
index = X / INTERVAL;//选取的映射函数
return index;
}
void BucketSort_plus(ElementType A[], int N)
//统一函数接口
{
Bucket B;
PNode temp;
int pos, i, j;
for (i = 0; i < BucketSize; i++)
B[i].head = B[i].last = NULL;//初始化桶头结点
for (j = 0; j < N; j++){
temp = (PNode)malloc(sizeof(struct Node));
pos = GetPosition(A[j]);//获取A[i]对应的桶下标
if (B[pos].head == NULL){
B[pos].last = (PNode)malloc(sizeof(struct Node));
B[pos].head = B[pos].last;
}
temp->Data = A[j];
temp->next = NULL;
//将A[i]丢入B[pos]内
B[pos].last->next = temp;
B[pos].last = temp;
}
//打印此时每个桶中的元素(排序之前)
printf("------before sorting------\n");
for (i = 0; i < BucketSize; i++){
printf("Bucket[%d]:", i);
PrintBuckets(B[i].head);
}
//对每个桶内的元素进行插入排序
//只对非空的桶进行排序
for (i = 0; i < BucketSize; i++){
if (B[i].head)
B[i].head->next = InsertionSort(B[i].head->next);
}
printf("------after sorting------\n");
//打印此时每个桶中的元素(排序之后)
for (i = 0; i < BucketSize; i++){
printf("Bucket[%d]:", i);
PrintBuckets(B[i].head);
}
//将每个桶内的元素顺序倒回A中
//只收集非空桶内的元素
for (i = BucketSize - 1, j = 0; i >= 0; i--){
if (B[i].head == NULL)
continue;
else{
B[i].head = B[i].head->next;
while (B[i].head){
A[j++] = B[i].head->Data;
B[i].head = B[i].head->next;
}
}
}
}
PNode InsertionSort(PNode bfr_list)
//对B[pos]内的元素进行插入排序
//从大到小进行排序
{
PNode p = bfr_list;
PNode k = bfr_list->next;
p->next = NULL;
while (k){
if (k->Data > p->Data){
//将k->Data与P的第一个元素进行比较
PNode temp;
temp = k;
k = k->next;
temp->next = p;
p = temp;
continue;
}
PNode ptr;
for (ptr = p; ptr->next != NULL; ptr = ptr->next){
if (ptr->next->Data < k->Data)
//将k->Data与p的第二个元素进行比较
break;
}
if (ptr->next){
PNode temp;
temp = k;
k = k->next;
temp->next = ptr->next;
ptr->next = temp;
continue;
}
else{
ptr->next = k;
k = k->next;
ptr->next->next = NULL;
continue;
}
}
return p;
}
void PrintBuckets(PNode bfr_list)
{
if (bfr_list == NULL)
printf("\n");
else{
bfr_list = bfr_list->next;
while(bfr_list){
printf("%d ", bfr_list->Data);
bfr_list = bfr_list->next;
}
}
printf("\n");
}
void Print(ElementType A[], int N)
//按照桶的编号顺序打印桶内元素
{
for (int i = 0; i < N; i++){
if (i == N - 1)
printf("%d\n",A[i]);
else
printf("%d ", A[i]);
}
}
int main(int argc, char const *argv[])
{
ElementType A[] = {49, 34, 31, 79, 73, 27, 49
, 56, 87, 35, 57, 10, 78, 67, 33, 98, 63, 47,
89, 46, 76, 23, 19, 2, 69, 3, 0, 1, 89, 14,};
int N = sizeof(A) / sizeof(int);
BucketSort_plus(A, N);
printf("--------result------\n");
Print(A, N);
system("pause");
return 0;
}
运行结果:
写在最后的东西(*・ω-q)
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,希尔排序中的子序列,归并排序中的子问题,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
-
时间复杂度分析:
- 循环计算每个关键字的桶映射函数,这个时间复杂度是 O ( N ) O(N) O(N);
- 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量;
很显然,上述两个部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:
- 映射函数 f ( k ) f(k) f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
- 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。当然,做到这一点很不容易,数据量巨大的情况下, f ( k ) f(k) f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
综上所述,对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O
(
N
)
+
O
(
M
∗
(
N
/
M
)
∗
l
o
g
(
N
/
M
)
)
=
O
(
N
+
N
∗
(
l
o
g
N
−
l
o
g
M
)
)
=
O
(
N
+
N
∗
l
o
g
N
−
N
∗
l
o
g
M
)
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
O(N)+O(M∗(N/M)∗log(N/M))=O(N+N∗(logN−logM))=O(N+N∗logN−N∗logM)
当
N
=
M
N = M
N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到
O
(
N
)
O(N)
O(N)。
最后的最后( ・´ω`・ )
桶排序的平均时间复杂度为线性的 O ( N + C ) O(N+C) O(N+C),其中 C = N ∗ ( l o g N − l o g M ) C=N*(logN-logM) C=N∗(logN−logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到 O ( N ) O(N) O(N)。 当然桶排序的空间复杂度为 O ( N + M ) O(N+M) O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
Reference
基数排序与桶排序,计数排序【详解】
面试中的排序算法总结
数据结构学习视频
数据结构学习笔记排序 (快速,计数排序,表排序,桶排序,基数排序)