C语言中如何实现桶排序
作者:NPC的克星
C语言实现桶排序
1.原理
由映射函数分配初始元素的键值,然后将这些元素放入对应键值的桶中,并对桶中的数据进行排序。然后依次将每个桶中的元素分出得到排好序的序列。
2.桶排序不是基于比较的排序
将N个待排序的元素放入桶中只需要O(n)时间。后续则是对桶中元素的排序,所以当桶越多的时候,桶中的元素会越少,所采取的基于比较的排序算法的时间则会大大减少。
所以,这里我们就可确定了一个重点,即是桶的数量必须是有限个的,可以经过一系列运算得到具体数目的。
3.桶的实现形式
我们以结构体数组存储单链表实现。以结构体数组的数组单元来春初链表的头节点,头节点含有两个变量,为指针变量(指向下一个链表节点),和整形变量key(就是如下图里面头节点的值),key表示链表的节点个数。
4.桶中元素的排序
因为桶是采取单链表来实现的,所以桶中元素的插入就是链表中的元素插入。这里要注意分桶为空和非空两种情况来插入。
if(p->key == 0){ bucket_table[index]->next = node_branch; (bucket_table[index]->key)++; } //链表的插入形式,按照大小从后到大。 else{ while(p->next!=NULL && p->next->key <= node_branch->key){ p=p->next; } node_branch->next = p->next; p->next = node_branch; (bucket_table[j]->key)++; }
4.最后就是将桶中的元素依次输出
或存放到数组原始序列的数组中。
5完整代码如下
#include<stdio.h> #include<stdlib.h> //整体思想大致为用数组单元内存放的为结构体式的链表,每个链表称为一个桶。通里面容纳的都是键值相同的元素。 // 之后便是查看对应元素的键值,然后放进与之对应的桶,还需注意桶为空和不空的时的放入方式 //桶元素的插入就是看桶以什么方式的实现。这里桶以链表的形式表现,所以桶中元素的插入即为链表中数组的插入。 /*只要桶的数量够多,那么之前的放入操作只需花费O(n)的时间,而后面的对每个桶里面的元素进行排序则需要基于比较的排序算法。因此后面算法的选择也是 关乎桶排序速度的重要因素。 */ //桶排序的特点是要有界限分明的桶,而不能是无限个桶,也就是说桶排序的个数应该是可以确定的,有限个的。 //这里链表实现桶排序的还有要注意的点,就是数组的首地址其实链表的头节点,有这里的值确定该桶的元素个数,并由这里出发寻找其他元素。 typedef struct node *Snode; typedef struct node{ int key; Snode next; }BBc; void sort(int keys[],int keys_size,int bucket_size) { Snode *bucket_table = (Snode *)malloc(bucket_size*sizeof(Snode));//为结构体数组分配空间。 for(int i=0;i<bucket_size;i++)//对每个数组单元赋予内存空间时,初始化每个结构体数组单元。 { bucket_table[i] = (Snode)malloc(sizeof(Snode));//这一步是必要的,因为之前只是给数组分配了一连串的存储空间,但是每个单元的存储地址都是 //不确定,也不确定该方式是否会自动地分配内存空间给每个数组单元。 //那么这样准确的给数组单眼分配的空间是占用之前的分配给数组的空间,还是重新分拨其他的空间给数组单元。 //其实应该是分配之前给整个数组单元分配的一段存储空间。 bucket_table[i]->key = 0; bucket_table[i]->next = NULL; }//其实创建数组这部分应该放在主函数那里,否则某些功能只能在这个函数中使用。 for(int j=0;j<keys_size;j++) { Snode node_branch = (Snode)malloc(sizeof(Snode));//定义一个节点,满足条件时链入以链表为表现形式的桶。 node_branch->key = keys[j]; node_branch->next = NULL; int index = keys[j]/10; Snode p = bucket_table[index];//p用来充当指向循环的变量。 //桶为空和非空时的两种插入形式 if(p->key == 0){ bucket_table[index]->next = node_branch; (bucket_table[index]->key)++; } //链表的插入形式,按照大小从后到大。 else{ while(p->next!=NULL && p->next->key <= node_branch->key){ p=p->next; } node_branch->next = p->next; p->next = node_branch; (bucket_table[j]->key)++; } } //以此输出每个桶中的所有元素。 for(int i=0;i<bucket_size;i++){ for(Snode k = bucket_table[i]->next;k!=NULL;k = k->next){ printf(" %d ",k->key); } } } int main() { int keys[] = {49,26,53,47,89,31,72,11,33}; int keys_size = sizeof(keys)/sizeof(int); int bucket_size = keys_size+2; sort(keys,keys_size,bucket_size); }
7.桶排序的时间复杂度和空间复杂度
前面的将n个待排序元素分到对应键值的桶中只需要O(n)时间,后面则是基于比较的排序算法,基于比较的排序算法最快可以达到:O(nlogn)时间。
所以桶里面的排序算法的选择也会影响到桶排序的速度。至于空间复杂度,一般都是占用空间比较大,以便每个桶中尽可能的达到一个元素,这样桶里面的排序也是O(n)时间,可以说是非常快速的。所以桶排序也是一种空间换时间的排序。
另外桶排序的元素键值应该相差不大,以免照成空间的浪费。另外,划分的桶也应该是有限个的。
【排序】图解桶排序
思想
一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。
桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。
图解过程
核心代码
public static void bucketSort(int[] arr){ // 计算最大值与最小值 int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; for(int i = 0; i < arr.length; i++){ max = Math.max(max, arr[i]); min = Math.min(min, arr[i]); } // 计算桶的数量 int bucketNum = (max - min) / arr.length + 1; ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum); for(int i = 0; i < bucketNum; i++){ bucketArr.add(new ArrayList<Integer>()); } // 将每个元素放入桶 for(int i = 0; i < arr.length; i++){ int num = (arr[i] - min) / (arr.length); bucketArr.get(num).add(arr[i]); } // 对每个桶进行排序 for(int i = 0; i < bucketArr.size(); i++){ Collections.sort(bucketArr.get(i)); } // 将桶中的元素赋值到原序列 int index = 0; for(int i = 0; i < bucketArr.size(); i++){ for(int j = 0; j < bucketArr.get(i).size(); j++){ arr[index++] = bucketArr.get(i).get(j); } } }
复杂度分析
1. 时间复杂度:O(N + C)
对于待排序序列大小为 N,共分为 M 个桶,主要步骤有:
- N 次循环,将每个元素装入对应的桶中
- M 次循环,对每个桶中的数据进行排序(平均每个桶有 N/M 个元素)
一般使用较为快速的排序算法,时间复杂度为O(NlogN),实际的桶排序过程是以链表形式插入的。
整个桶排序的时间复杂度为:
O(N)+O(M*(N/M*log(N/M)))=O(N*(log(N/M)+1))
当 N = M 时,复杂度为 O(N)
2. 额外空间复杂度:O(N + M)
稳定性分析
桶排序的稳定性取决于桶内排序使用的算法。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。