桶排序详解

桶排序

  • 入门介绍⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
  如果已知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

  1. 分析小朋友们的分数,可以发现分数均在0-9之间,因此呢我们就可以准备10个桶,编号分别为0-9,也就是申请一个大小为10的数组Bucket[10]。
  2. 在排序之前,先对每个桶初始化,即Bucket[0]-Bucket[9]均初始化为0,表示这些分数还没有小朋友得过。
  3. 顺序扫描A[20]这个数组,每遇到一个小朋友的分数,就找该分数对应哪个桶,找到了就把分数丢到桶里去,然后桶里面的数+1,每丢一次就+1,因为所有分数都在0-9的范围内且不存在小数,因此最后所有的分数都能被丢到相应的桶里,也就是下面那幅图。
  4. 都丢完后,每个桶里的数字是几,就代表这个桶对应的分数出现了几次。按照桶从大到小的顺序,将出现的分数打印出来,出现几次打印几次就行了。
    在这里插入图片描述

  • 完整代码(参考)φ(>ω<*)

这里笔者分别使用数组和链表来进行表示

#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(logNlogM))=O(N+NlogNNlogM)
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(logNlogM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到 O ( N ) O(N) O(N)。 当然桶排序的空间复杂度为 O ( N + M ) O(N+M) O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的


Reference

基数排序与桶排序,计数排序【详解】
面试中的排序算法总结
数据结构学习视频
数据结构学习笔记排序 (快速,计数排序,表排序,桶排序,基数排序)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuuyann

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值