桶排序总结

假如对数组nums进行桶排序,nums的长度为L,最小元素为A,最大元素为B。

则gap为(B-A)/L+1,桶的个数为(B-A)/gap+1。

另外一个重要的性质是,同一个桶中的元素相差最多为gap-1。

对nums中的元素nums[i],确定放入哪个桶的公式为:(nums[i]-A)/gap

 

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

1. 基本思想

桶排序的思想近乎彻底的分治思想。

桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。

然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。

接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。

补充: 映射函数一般是 f = array[i] / k; k^2 = n; n是所有元素个数

为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

 

1. 桶排序介绍

桶排序(Bucket sort)是一种基于计数的排序算法,工作的原理是将数据分到有限数量的桶子里,然后每个桶再分别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。当要被排序的数据内的数值是均匀分配的时候,桶排序时间复杂度为Θ(n)。桶排序不同于快速排序,并不是比较排序,不受到时间复杂度 O(nlogn) 下限的影响。

桶排序按下面4步进行:

  • 1. 设置固定数量的空桶。
  • 2. 把数据放到对应的桶中。
  • 3. 对每个不为空的桶中数据进行排序。
  • 4. 拼接从不为空的桶中数据,得到结果。

桶排序,主要适用于小范围整数数据,且独立均匀分布,可以计算的数据量很大,而且符合线性期望时间。

2. 桶排序算法演示

举例来说,现在有一组数据[7, 36, 65, 56, 33, 60, 110, 42, 42, 94, 59, 22, 83, 84, 63, 77, 67, 101],怎么对其按从小到大顺序排序呢?

技术分享

操作步骤说明:

  • 1. 设置桶的数量为5个空桶,找到最大值110,最小值7,每个桶的范围20.8=(110-7+1)/5 。
  • 2. 遍历原始数据,以链表结构,放到对应的桶中。数字7,桶索引值为0,计算公式为floor((7 – 7) / 20.8), 数字36,桶索引值为1,计算公式floor((36 – 7) / 20.8)。
  • 3. 当向同一个索引的桶,第二次插入数据时,判断桶中已存在的数字与新插入数字的大小,按照左到右,从小到大的顺序插入。如:索引为2的桶,在插入63时,桶中已存在4个数字56,59,60,65,则数字63,插入到65的左边。
  • 4. 合并非空的桶,按从左到右的顺序合并0,1,2,3,4桶。
  • 5. 得到桶排序的结构

3.桶排序c++代码实现

 

 

 

// 8-4.桶排序.cpp : 定义控制台应用程序的入口点。
//
 
#include <stdio.h>
#include <stdlib.h>
 
//链表结点描述  
typedef struct Node{  
    double key;  
    struct Node * next;   
}Node;  
//辅助数组元素描述  
typedef struct{  
    Node * next;  
}Head; 
 
void bucketSort(double* a,int n)  
{  
    int i,j;  
    Head head[10]={NULL};  
    Node * p;  
    Node * q;  
    Node * node;  
    for(i=0;i<=n;i++){  
        node=(Node*)malloc(sizeof(Node));  
        node->key=a[i];  
        node->next=NULL;  
        p = q =head[(int)(a[i]*10)].next;  
        if(p == NULL){  
            head[(int)(a[i]*10)].next=node;  
            continue;  
        }  
        while(p){  
            if(node->key < p->key)  
                break;  
            q=p;  
            p=p->next;  
        }  
        if(p == NULL){  
            q->next=node;  
        }else{  
            node->next=p;  
            q->next=node;  
        }  
    }  
    j=0;  
    for(i=0;i<10;i++){  
        p=head[i].next;  
        while(p){  
            a[j++]=p->key;  
            p=p->next;  
        }  
    }  
}
 
int main(int argc, char* argv[])
{
    int i;  
    double a[13]={0.5,0.13,0.25,0.18,0.29,0.81,0.52,0.52,0.83,0.52,0.69,0.13,0.16};
    bucketSort(a,12);  
    for(i=0;i<=12;i++)  
        printf("%-6.2f",a[i]);  
    printf("\n");  
    return 0;
}
View Code

 

4.桶排序代价分析

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。

对N个关键字进行桶排序的时间复杂度分为两个部分:

(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:

(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。

(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:

O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)

当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。

总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

我个人还有一个感受:在查找算法中,基于比较的查找算法最好的时间复杂度也是O(logN)。比如折半查找、平衡二叉树、红黑树等。但是Hash表却有O(C)线性级别的查找效率(不冲突情况下查找效率达到O(1))。大家好好体会一下:Hash表的思想和桶排序是不是有一曲同工之妙呢?

posted @ 2021-01-29 10:24  钢与铁  阅读(273)  评论(0编辑  收藏  举报