经典排序算法系列9----分配排序(桶排序和基数排序)

桶排序和基数排序均属于分配排序。分配排序的基本思想:排序过程无须比较关键字,而是通过用额外的空间来"分配"和"收集"来实现排序,它们的时间复杂度可达到线性阶:O(n)。简言之就是:用空间换时间,所以性能与基于比较的排序才有数量级的提高!

桶排序(Bucket Sort),也称箱排序
基本思想:设置若干个箱子,依次扫描待排序的记录 array[0],array[1],…,array[n - 1],把关键字等于 k 的记录全都装入到第 k 个箱子里(分配),然后按序号依次将各非空的箱子里的记录收集起来,从而完成排序。

桶排序所需要的额外空间取决于关键字的个数,若 array[0..n - 1] 中关键字的取值范围是 0 到 m - 1 的整数,则必须设置 m 个箱子。因此箱排序要求关键字的类型是有限类型,否则可能要无限个箱子。一般情况下每个箱子中存放多少个关键字相同的记录是无法预料的,故箱子的类型应设计成链表为宜。

在实现上,桶排序一般采用另外的变种。即:把 [0, m) 划分为 m 个大小相同的子区间,每一子区间是一个桶。然后将 n 个记录分配到各个桶中。因为关键字序列是均匀分布在[0,m)上的,所以一般不会有很多个记录落入同一个桶中。由于同一桶中的记录其关键字不尽相同,所以必须采用关键字比较的排序方法(通常用插入排序)对各个桶进行排序,然后依次将各非空桶中的记录收集起来即可。

C 代码实现:

struct bucket_node {
    int key;
   bucket_node* next;
};
// 取得数组中最大数的位数
int get_max_digital_count(int* array, int length)
{
    assert(array && length > 0);

    int i = 0;
    int max = array[0];
    int maxDigitalCount = 0;

    for (i = 1; i < length; i++) {
        if (max < array[i]) {
            max = array[i];
        }
    }

  while(max > 0)
  {
    max /= 10;
    ++maxDigitalCount;
  }

    return maxDigitalCount;
}

// 取得数 num 中从低到高第 n 位上的数字
int get_ditital_at(int num, int n)
{
    while (--n > 0) {
        num /= 10;
    }

    return (num % 10);
}

// 箱/桶排序
//
void bucket_sort(int* array, int length)
{
    assert(array && length >= 0);

    if (length <= 1) {
        return;
    }

    int i, index;
    bucket_node* temp = NULL;
    bucket_node bucket[10] = {0, };    // 根据数字个数 0 ~ 9 建立 10 个桶

    int count = get_max_digital_count(array, length);

    // 建立数据节点
    bucket_node* data = (bucket_node*)malloc(length * sizeof(bucket_node));
    if (!data) {
        printf("Error: out of memory!/n");
        return;
    }

    for (i = 0; i < length; i++) {
        data[i].key = array[i];
        data[i].next = NULL;
    }

    // 分配
    for (i = 0; i < length; i++) {
        index = get_ditital_at(data[i].key, count);
        if (bucket[index].next == NULL) {
            bucket[index].next = &data[i];
        }
        else {
            temp = &bucket[index];
            while (temp->next != NULL && temp->next->key < data[i].key) {
                temp = temp->next;
            }

            data[i].next = temp->next;
            temp->next = &data[i];
        }
    }

    // 收集
    index = 0;
    for (i = 0; i < 10; i++) {
        temp = bucket[i].next;
        while (temp != NULL) {
            array[index++] = temp->key;
            temp = temp->next;
        }
    }


    free(data);
}

时间复杂度分析:桶排序的平均时间复杂度是线性的,即 O(n)。但最坏情况仍有可能是 O(n ^ 2)。

空间复杂度分析:桶排序只适用于关键字取值范围较小的情况,否则所需箱子的数目 m 太多导致浪费存储空间和计算时间。

基数排序(Radix Sort)基本思想:基数排序是对桶排序的改进和推广。如果说桶排序是一维的基于桶的排序,那么基数排序就是多维的基于桶的排序。我这么说,可能还不是太清楚。比方说:用桶排序对 [0, 30] 之间的数进行排序,那么需要 31 个桶,分配一次,收集一次,完成排序;那么基数排序则只需要 0 - 9 总共 10 个桶(即关键字为数字 0 - 9),依次进行个位和十位的分配和收集从而完成排序。

C 代码实现:

 // 基数排序
//
void radix_sort(int* array, int length)
{
    assert(array && length >= 0);

    if (length <= 1) {
        return;
    }

    const int buffer_size = length * sizeof(int);

    int i, k, count, index;
    int bucket[10] = {0, };    // 根据数字个数 0 ~ 9 建立 10 个桶

    int* temp = (int*)malloc(buffer_size);
    if (!temp) {
        printf("Error: out of memory!/n");
        return;
    }

    count = get_max_digital_count(array, length);

    for (k = 1; k <= count; ++k) {
        memset(bucket, 0, 10 * sizeof(int));

        // 统计各桶中元素的个数
        for (i = 0; i < length; ++i) {
            index = get_ditital_at(array[i], k);
            ++bucket[index];
        }

        // 为每个记录创建索引下标
        for (i = 1; i < 10; ++i) {
            bucket[i] += bucket[i - 1];
        }

        // 按索引下标顺序排列
        for (i = length - 1; i >= 0; --i) {
            index = get_ditital_at(array[i], k);
            assert(bucket[index] - 1 >= 0);
            temp[--bucket[index]] = array[i];
        }

        // 一趟桶排序完毕,拷贝结果
        memcpy(array, temp, buffer_size);

#ifdef DEBUG_SORT
        debug_print(" 第 %d 趟排序:", k);
        for (i = 0; i < length; ++i) {
            debug_print("%d ", array[i]);
        }

        debug_print("/n");
#endif
    }

    free(temp);
}

时间复杂度分析:基数排序的时间负责度为 O(n)。

空间复杂度:基数排序所需的辅助存储空间为 O(n + r * d),其中 r 为记录中关键字分量的最大个数,d 为关键字的个数。比如说:待排序为 0 - 999,那么分量的最大个数为 3,关键字的个数为 10(0 - 9)。

补充:基数排序是稳定的。

若排序文件不是以数组 array 形式给出,而是以单链表形式给出(此时称为链式的基数排序),则可通过修改出队和入队函数使表示箱子的链队列无须分配结点空间,而使用原链表的结点空间。人队出队操作亦无需移动记录而仅需修改指针。虽然这样一来节省了一定的时间和空间,但算法要复杂得多,且时空复杂度就其数量级而言并未得到改观。


测试:在前文《排序算法之插入排序》测试代码的基础上添加两行代码即可:
{"桶/箱排序", bucket_sort},
{"基数排序", radix_sort},

测试结果:
=== 桶/箱排序 ===
original: 65 32 49 10 8 72 27 42 18 58 91
sorted: 8 10 18 27 32 42 49 58 65 72 91

original: 10 9 8 7 6 5 4 3 2 1 0
sorted: 0 1 2 3 4 5 6 7 8 9 10

=== 基数排序 ===
original: 65 32 49 10 8 72 27 42 18 58 91
第 1 趟排序:10 91 32 72 42 65 27 8 18 58 49
第 2 趟排序:8 10 18 27 32 42 49 58 65 72 91
sorted: 8 10 18 27 32 42 49 58 65 72 91

original: 10 9 8 7 6 5 4 3 2 1 0
第 1 趟排序:10 0 1 2 3 4 5 6 7 8 9
第 2 趟排序:0 1 2 3 4 5 6 7 8 9 10
sorted: 0 1 2 3 4 5 6 7 8 9 10


推荐阅读:
经典排序算法系列1----冒泡排序的实现
经典排序算法系列2----插入排序的实现
经典排序算法系列3----直接选择排序及交换二个数据的正确实现
经典排序算法系列4----希尔排序的实现
经典排序算法系列5----归并排序
经典排序算法系列6----快速排序
经典排序算法系列7----堆与堆排序
经典排序算法系列8----7大排序算法总结篇
经典排序算法系列9----分配排序(桶排序和基数排序)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,101评论 0 52
  • 概述排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的...
    Luc_阅读 2,210评论 0 35
  • 概述:排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    每天刷两次牙阅读 3,704评论 0 15
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,174评论 0 2
  • (本文参加#感悟三下乡,青春筑梦行#活动。本人承诺,本文是原创,且并未在其他平台上发表过。) 暑期社会实践活动是我...
    溪影阅读 167评论 0 0