当前位置: 首页 > news >正文

数据结构与算法分析:你真的理解排序算法吗——桶排序(代码详解)

一、算法描述

桶排序又称箱排序。当计算集合中的个元素时,如果能够构建一个小得多的 k 个值的集合,那么这个时候就可以使用计数排序。给定个元素的集合,桶排序构造了 n 个桶来切分输人集合,因此桶排序能够降低处理时其在额外空间的开销。如果一个散列函数,hash(A),用来均匀地将 n 个元素分配到 n 个桶中,那么如图 4-18 所示的桶排序在最坏情况下也能够以 O(n)的时间进行排序。如果希望使用桶排序,那么需要满足以下两点。
均匀分布
输入数据需要均匀分布在一个给定的范围内。基于这种分配,算法创建了个桶来平分输入数据。
有序散列函数
桶必须是有序的。也就是说,如果 i<j,桶 b,中的元素要字典序小于桶 b 中的元素。
桶排序不适合排序随机字符串,但是,它能够被用来排序在区间[0,1)间均匀分配的浮点数。
一旦所有待排序的元素都被放入桶中,桶排序从左至右对每个桶中的值使用插入排序。
每一个桶中的元素按照顺序从左至右抽取出来重新构造原始的数组。

以下是桶排序的过程:
一、确定桶的数量和大小
1.获取取值范围:首先,需要确定待排序数组的最大值和最小值,从而得到数组的取值范围。
2.确定桶的数量:根据数组的取值范围和期望的桶的数量(或根据某种映射规则),将取值范围划分为若干个小区间,每个小区间对应一个桶。桶的数量可以是一个预设的常数,也可以根据数组的大小和取值范围动态确定。
3.确定桶的大小:每个桶的大小可以是固定的,也可以是根据取值范围均匀划分的。桶的大小和数量共同决定了桶排序的效率和效果。
二、分配元素到桶中
1.映射函数:使用一个映射函数f,将待排序数组中的每个元素映射到对应的桶中。映射函数的选择对于桶排序的效率至关重要,它应该能够将元素均匀地分配到各个桶中。
2.分配元素:遍历待排序数组,根据映射函数将每个元素分配到对应的桶中。可以使用数组、链表、队列等数据结构来实现桶的存储。
三、对每个桶中的元素进行排序
1.选择排序算法:对每个非空的桶中的元素进行排序。可以选择任何有效的排序算法,如插入排序、快速排序、堆排序等。选择的排序算法应该根据桶中元素的数量和桶排序的整体效率来决定。
2.排序桶内元素:对每个桶中的元素应用选定的排序算法进行排序。
四、合并桶中的元素
1.按顺序合并桶:将所有非空桶中的元素按顺序合并起来,形成一个有序的数组。合并的过程可以按照桶的顺序依次进行,也可以采用其他有效的合并算法。
2.得到有序序列:合并完成后,得到的数组就是有序序列。
五、注意事项
1.元素分布:桶排序的效果取决于待排序数组中元素的分布。如果元素分布极不均匀,那么某些桶中可能会包含大量的元素,导致桶内排序的效率降低。因此,在选择桶排序时,需要注意待排序数组中元素的分布情况。
2.映射函数:映射函数的选择对于桶排序的效率至关重要。一个好的映射函数应该能够将元素均匀地分配到各个桶中,从而避免某些桶中元素过多或过少的情况。
3.桶内排序算法:桶内排序算法的选择也会影响桶排序的整体效率。在选择桶内排序算法时,需要根据桶中元素的数量和桶排序的整体效率来决定。

二、复杂度分析

在这里插入图片描述
在sortPointers 函数中,输入中的每一个元素根据提供的散列函数,被插入到相应的桶中,花费是线性时间 O ( n ) O(n) On。桶中的元素并不是有序的,但是由于仔细设计的散列函数,我们知道,如果 i<j,在桶 b i b_{i} bi中的所有的元素都小于 b j b_{j} bj中的所有元素。
随着值从桶中抽取出来并且写回到输入数组,当一个桶包含多个元素的时候就要使用插入排序。对于表现出 O(n)性能的桶排序,我们需要保证排序每个桶所花费的总时间为 O(n)。我们定义 n,是分到桶 b,的元素数目。我们能够将 n,看做随机变(使用统计理)论)。现在考虑 n i n_{i} ni的期望值 E [ n i ] E[n_{i}] E[ni]。输入中的每一个元素插入到一一个给定的桶中的概率为 1/p,因为每个元素的值是从[0,1)之间均匀抽取的。因此 E [ n i ] = n ∗ p = n ∗ ( 1 / n ) = 1 E[n_{i}]=n^{*}p=n^{*}(1/n)=1 E[ni]=np=n(1/n)=1,方差 V a r [ n i ] = n ∗ p ∗ ( 1 − p ) = ( 1 − 1 / n ) Var[n_{i}]{=}n^{*}p^{*}(1-p){=}(1-1/n) Var[ni]=np(1p)=(11/n)。考虑方差是非常重要的,因此有些标可能是空的,而其他的可能有多于一个元素;我们需要保证没有桶包含过多的元素。我们再一次通过统计理论得到下面的等式:
E [ n i 2 ] = V a r [ n i ] + E 2 [ n i ] E[n_{i}^{2}]=Var[n_{i}]+E^{2}[n_{i}] E[ni2]=Var[ni]+E2[ni]
从这个等式我们能够计算出 n2 的期望值。这个计算是非常关键的,因为这是决定插入排序开销的关键因素,插入排序的最坏情况性能为 O ( n 2 ) O(n^{2}) On2。我们计算出 E [ n i 2 ] = ( 1 − 1 / n ) + 1 = ( 2 − 1 / n ) E[n_{i}^{2}]=(1-1/n)+1=(2-1/n) E[ni2]=11/n+1=21/n,可以看到 E [ n i 2 ] E[n_{i}^{2}] E[ni2]是一个常数。这个意味着当我们将在 n 个桶上执行插入排序的总开销加起来之后,总的期望性能为 O ( n ) O(n) On

三、适用情况

当待排序元素能够使用一个快速散列函数均匀分配时,桶排序是最快的排序法。
如果存储空间不重要,而且元素服从一个全序关系,桶排序就能够利用这些信息,节省大量开销。
以下是几种适用情况:

  1. 数据分布均匀
    均匀分布:当数据在整个取值范围内均匀分布时,桶排序能够确保每个桶包含近似相等数量的数据元素。这种情况下,桶排序的效率非常高,因为每个桶的排序负担相对均衡。
    避免极端情况:如果数据分布极端不均匀,例如所有数据都集中在某个小范围内,那么桶排序的效率会显著降低。因为某些桶可能会变得非常拥挤,而其他桶则可能几乎为空,导致桶内排序的时间复杂度增加。
  2. 数据量较大
    高效处理大数据:桶排序在处理大数据集时通常比简单的比较排序算法(如快速排序、归并排序)更高效。这是因为桶排序避免了大量的元素间比较操作,而是通过将元素分配到不同的桶中来实现排序。
    内存需求:然而,需要注意的是,桶排序需要额外的内存空间来存储桶。因此,在处理大数据时,需要确保有足够的内存资源来支持桶排序。
  3. 数据范围已知
    确定桶的数量和大小:当数据的取值范围已知时,可以更容易地确定桶的数量和大小。这有助于确保桶排序的性能和效率。
    优化桶的分配:如果数据的取值范围很小,可以考虑使用固定大小的桶;如果取值范围很大,则可能需要动态地调整桶的大小和数量。
  4. 桶内排序算法选择灵活
    多种排序算法可选:桶排序的一个优点是,它允许在每个桶内使用不同的排序算法。这可以根据桶内数据的特性和数量来选择最合适的排序算法。
    优化整体性能:通过为每个桶选择适当的排序算法,可以进一步优化桶排序的整体性能。
  5. 数据具有某种模式或周期性
    利用模式进行排序:在某些情况下,数据可能具有某种模式或周期性。桶排序可以利用这些模式来更有效地分配和排序数据。
    提高排序效率:通过识别并利用数据中的模式或周期性,桶排序可以进一步提高排序效率。

四、算法实现

#include<stdio.h>
#include<stdlib.h>//extern int hash(void* elt);
//extern int numBuckets(int numElements);//对于从[0, 1)中均匀选择的数字,下面即hash和numBuckets函数实现的例子://桶的数量
static int num = 0;//使用的桶数量和元素数量一样
int numBuckets(int numElements)
{num = numElements;return numElements;
}
//散列函数将元素映射到桶中,因为散列函数的值的范围是[0,1);所以我们将每个桶的大小分为1/num
int hash(double* d)
{int bucket = num * (*d);return bucket;
}//桶中的元素链表
typedef struct entry
{void* element;struct entry* next;
}ENTRY;typedef struct
{int size;ENTRY* head;
}BUCKET;//桶的指针
static BUCKET* buckets = 0;int cmp(const void* x, const void* y)
{int standard;//这里比较时也是需要做强制类型转换standard = *(int*)x - *(int*)y;return standard;
}//一个接一个移除,并且覆盖ar
void extract(BUCKET* buckets, int(*cmp)(const void*, const void*), void** ar, int n)
{int i;int low;int idx = 0;for (i = 0;i < num;i++){ENTRY* ptr, * tmp;//空桶if (buckets[i].size == 0){continue;}ptr = buckets[i].head;if (buckets[i].size == 1){ar[idx++] = ptr->element;free(ptr);buckets[i].size = 0;continue;}//对链表中的元素执行插入排序,然后插入到数组中。然后释放链表low = idx;ar[idx++] = ptr->element;tmp = ptr;ptr = ptr->next;free(tmp);while (ptr != NULL){int i = idx - 1;while (i >= low && cmp(ar[i], ptr->element) > 0){ar[i + 1] = ar[i];i--;}ar[i + 1] = ptr->element;tmp = ptr;ptr = ptr->next;free(tmp);idx++;}buckets[i].size = 0;}
}void sortPointers(void** ar, int n, int(*cmp)(const void*, const void*))
{int i;num = numBuckets(n);buckets = (BUCKET*)calloc(num, sizeof(BUCKET));for (i = 0;i < n;i++){int k = hash(ar[i]);//插入每个元素并且增加计数ENTRY* e = (ENTRY*)calloc(1, sizeof(ENTRY));e->element = ar[i];if (buckets[k].head == NULL){buckets[k].head = e;}else{e->next = buckets[k].head;buckets[k].head = e;}buckets[k].size++;}//读出并且覆盖arextract(buckets, cmp, ar, n);free(buckets);
}int main()
{int data[] = { 34, 7, 23, 32, 5, 62, 32, 15, 78, 9 };int n = sizeof(data) / sizeof(data[0]);//在 main 函数内部声明变长指针数组//int* dataPtrs[n];int**  dataPtrs = (int**)malloc(sizeof(int) * n);// 填充数据指针数组for (int i = 0; i < n; i++){dataPtrs[i] = &data[i];}// 排序sortPointers(dataPtrs, n, cmp);// 打印排序后的数组printf("Sorted array:\n");for (int i = 0; i < n; i++) {printf("%d ", *dataPtrs[i]);}printf("\n");return 0;
}

这里留一个问题,如何将这段C代码转换为C++代码,并成功在VS中运行,该代码在Clion中能成功运行,但是VS不支持C99标准,而且C++不支持C语言的默认强制类型转化,所以无法直接在VS中运行。

五、算法优化

在散列排序中,每一个桶代表的是散列函数返回的唯一值。散列排序并没有创建n个
桶,而是创建了k个桶,随着k的增长,散列排序的速度不断加快。散列排序的关键在于
散列函数hash(e)返回的整数值,对于每个元素e,如果asa,那么hash(a)shash(a)。
下面定义了散列函数hash(e),这个函数计算仅仅包含了小写字母的元素。它将字符
串的前三个字符转换成一个值(基准值为26),对于字符串“abcdefgh”,它的前三个
字符(“abc”)被抽取出来然后转换得0676+126+2=28。这个字符串将插入到标为28
的桶中。

//使用的桶数量。
int  numBuckets(int  numElements)
{return  26 * 26 * 26;
}//散列函数计算出每个元素应映射到的桶。
int  hash(void* elt)
{return  (((char*)elt)[o] - 'a') * 676 +(((char*)elt)[1] - 'a') * 26 +(((char*)elt)[2] - 'a');
}

散列排序的性能和桶的大小以及输入的规模有关,具体可参考下表。我们将一个使用了
三中值方法来选择pivotIndex的快速排序和散列排序进行比较,并且给出了一个可以比
较的排序时间。
在这里插入图片描述
注意在17576个桶,n>8192个元素的情况下,散列排序的性能要优于快速排序(并且这
个趋势随着n的增长继续保持)。但是,仅仅只有676个桶,n>32768(平均每个桶48个
元素)的话,散列排序随着数据集的增长,执行插入排序的开销也不断增长,速度越来
越慢。事实上,在26个桶,n>256的情况下,随着n的加倍,散列排序所需的时间增加3
倍,在桶很少时,散列排序的性能是 O ( n 2 ) O(n^2) O(n2)

六、引用及参考文献

1.《算法设计手册》


http://www.mrgr.cn/news/59128.html

相关文章:

  • OAK相机的RGB-D彩色相机去畸变做对齐
  • vue父子传参的方式——Prop
  • IDEA中一个窗口打开多个项目-区别于eclipse
  • day02|计算机网络重难点之HTTP请求报文和响应报文、HTTP的请求方式(方法字段)、GET请求和POST请求的区别
  • springboot医疗物品采购系统-计算机设计毕业源码10210
  • 通过 Lighthouse 和 speed-measure-webpack 插件分析优化构建速度与体积
  • redis高级篇之IO多路复用select方法简介 第174节答疑
  • 基于DDPG算法的股票量化交易
  • 【项目实战】HuggingFace初步实战,使用HF做一些小型任务
  • 如何快速绘制高效的业务架构图(三步完成)
  • 【动手学深度学习】8.5 循环神经网络从零开始实现
  • 跟我学C++中级篇——volatile的探究
  • 大厂项目经理推荐的10款常用的项目管理软件值得你收藏
  • Java中TreeSet的使用
  • 代码随想录算法训练营第二十七天|Day27 贪心算法
  • 博图软件的使用(一)
  • 006:看图软件ACDSeePhotoStudio2019安装教程
  • Python中的Bloom Filter算法详解
  • C/C++(七)RAII思想与智能指针
  • Java-图书管理系统
  • 专题十六_栈_队列_优先级队列_算法专题详细总结
  • java核心技术点都有哪些
  • 【C++ 真题】B2099 矩阵交换行
  • 蓝桥杯第二十场小白入门赛
  • 走廊泼水节——求维持最小生成树的完全图的最小边权和
  • 010 操作符详解 上