十大排序算法总结(c语言实现)(三)
桶排序,基数排序,计数排序
前言
上一节中基于树结构的排序算法的时间复杂度极限为o(nlogn),这一节通过散列表来实现的排序算法能够进一步提升算法的时间复杂度,这也是所有算法中平均时间复杂度最低的算法。
一、桶排序
1.原理
对9,25,56,8,38,99,9,90进行排序。
每个桶的范围,左闭右开区间ceil(((99+1)-8)/10)=10。
然后计算每个值落在哪个桶中,(val-8)/10
最后将每个数插入到散列表中如图:
从散列表中从左到右,从下倒上一次遍历每个节点,最后得到排序结果:8,9,9,25,38,56,90,99
2.实现
#define BUCKET_SIZE 10 //有多少个桶
typedef struct NODE_S {
int value;
NODE_S* next;
}LISTNODE;//链表节点
void free_list(LISTNODE** phead)
{ //释放链表
for (int i = 0; i < BUCKET_SIZE; i++)
{
if (phead[i] == NULL) continue;
else {
LISTNODE* cur = phead[i];
while (cur != NULL) {
LISTNODE* pnext = cur->next;
free(cur);
cur = pnext;
}
}
}
free(phead);
}
int get_max(int* arr, int len)
{ //获得数组中的最大数
int max = INT_MIN;
for (int i = 0; i < len; i++)
{
if (max < arr[i]) max = arr[i];
}
return max;
}
int get_min(int* arr, int len)
{ //获得数组中的最小数
int min = INT_MAX;
for (int i = 0; i < len; i++)
{
if (min > arr[i]) min = arr[i];
}
return min;
}
int get_key(int val,int max,int min)
{ //获取val值的key,左闭右开区间
double temp = (double)(max+1 - min) / (double)BUCKET_SIZE;
int sub = ceil(temp);//每个桶装多少个数
int key = (val - min) / sub; //val落在哪个桶
return key;
}
void insert_node(LISTNODE** node, int val)
{ //向散列表中顺序插入节点
LISTNODE* new_node = (LISTNODE*)malloc(sizeof(LISTNODE));//初始化新的链表节点
new_node->value = val;
new_node->next = NULL;
if (*node == NULL)
{
*node = new_node;//散列表为空,该节点就为头部节点
}
else {
LISTNODE* cur = *node;//头结点的值大于待插入节点的值,待插入节点作为头结点
if (cur->value > val)
{
*node = new_node;
new_node->next = cur;
}
else {
LISTNODE* pre = NULL;//头结点值小于等于待插入值,找到合适位置插入val
while (cur != NULL && cur->value <= val)
{
pre = cur;
cur = cur->next;
}
pre->next = new_node;
new_node->next = cur;
}
}
}
void bucket_sort(int* arr, int len)
{ //桶排序主函数
LISTNODE** phead = (LISTNODE**)malloc(sizeof(LISTNODE*) * BUCKET_SIZE);
for (int i = 0; i < BUCKET_SIZE; i++)
{
phead[i] = NULL;
}
int max = get_max(arr, len);
int min = get_min(arr, len);
for (int i = 0; i < len; i++)
{
int key = get_key(arr[i], max, min);
insert_node(&phead[key], arr[i]);//将数组中的数全部插入散列表中
}
int index = 0;
for (int i = 0; i < BUCKET_SIZE; i++)
{ //挨着遍历所有的桶按顺序取出每个链表上的数
LISTNODE* temp = phead[i];
if (temp == NULL) continue;
else {
while (temp != NULL)
{
arr[index++] = temp->value;
temp = temp->next;
}
}
}
free_list(phead);//排序完成释放链表
}
3.算法分析
时间复杂度: 带排序个数记为n,桶的个数记为k。 将待排序数值插入散列表中的时间复杂度为o(n),最后对散列表进行遍历时间复杂度为o(k),所以平均和最好的时间复杂度为o(n+k)。最坏的情况是大部分数字都集中到一个桶中了,一个桶的链表过长,这时的时间复杂度为o(n^2)。
空间复杂度: 需要额外空间桶和排序节点,所以空间复杂度为o(n+k)。
稳定排序: 当两个节点相同时,可以按照一定顺序插入两个节点,所以是稳定性排序。
二、基数排序
1.原理
对每个关键码进行桶排序,最后得到排序结果。例如:对年-月-日进行排序,分别对日-月-年进行桶排序,最后就会得到按照年-月-日时间的排序;对三位数进行排序,分别对个-十-百单个数组进行排序,最后就会得到三位数的排序。
例子:对8,110,99,50,8,3,10,666进行基数排序。
对个位数进行桶排序(插入时没有对链表进行排序)。结果得:110,50,10,3,666,8,8,99
在对个位数排序的基础上对十位数进行桶排序。结果得:3,8,8,110,10,50,666,99,
在对十位的基础上对百位数进行桶排序。结果得:3,8,8,10,50,99,110,666,这就是最终结果。
2.实现
void insert_node(LISTNODE** node, int val)
{
LISTNODE* new_node = (LISTNODE*)malloc(sizeof(LISTNODE));
new_node->value = val;
new_node->next = NULL;
if (*node == NULL)
{
*node = new_node;
}
else {
LISTNODE* cur = *node;
while(cur->next != NULL)
{
cur = cur->next;
}
cur->next = new_node;
}
}
int get_max_count(int* arr, int len)
{ //获取最大值得位数
int max = 0;
int count = 0;
for (int i = 0; i < len; i++)
{
if (max < arr[i])
{
max = arr[i];
}
}
while (max)
{
max = max / 10;
count++;
}
return count;
}
void cardinal_sort(int* arr, int len)
{
LISTNODE** phead = (LISTNODE**)malloc(sizeof(LISTNODE*) * BUCKET_SIZE);
int loop = get_max_count(arr, len);
int temp = 1;
while (loop--)//需要进行几次桶排序
{
for (int i = 0; i < BUCKET_SIZE; i++)
{
phead[i] = NULL;//初始化桶
}
for (int i = 0; i < len; i++)
{
int key = (arr[i] / temp) % 10;//分别按个十百的对每个位进行排序
insert_node(&phead[key], arr[i]);
}
temp *= 10;
int index = 0;
for (int i = 0; i < BUCKET_SIZE; i++)
{
LISTNODE* temp = phead[i];//按顺序取出散列表中的值
if (temp == NULL) continue;
else {
while (temp != NULL)
{
arr[index++] = temp->value;
temp = temp->next;
}
}
}
}
free_list(phead);
}
3.算法分析
时间复杂度: 待排序个数为n,桶的个数为k,关键码为t,则平均时间复杂度为o(t(n+k))。
空间复杂度: 和桶排序所需要的空间相同,空间复杂度为o(n+k)。
稳定排序: 和桶排序相同,为稳定性排序
三、计数排序
1.原理
分配一个数组用于统计待排序数组中数字出现的个数,用排序数组的数字作为统计数组的下表,统计数组中的值为排序数组中数字出现的个数。
例如:对3,2,5,3,2进行排序
分配一个大小为6的数组空间,利用该数组对出现的数字进行统计得:0,0,2,2,0,1;最后遍历这个数组获得排序数组,遍历到下表为2时,值为2,则排序数组为2,2。遍历完整个统计数组则排序数组为2,2,3,3,5。
2.实现
int get_max(int* arr, int len)
{ //获得数组中的最大数
int max = INT_MIN;
for (int i = 0; i < len; i++)
{
if (max < arr[i]) max = arr[i];
}
return max;
}
void count_sort(int* arr, int len)
{
int vec_size = get_max(arr, len)+1;
int* vec = (int*)malloc(sizeof(int) * vec_size);
memset(vec, 0, vec_size*sizeof(int));//初始化统计个数的数组
for (int i = 0; i < len; i++)
{
vec[arr[i]]++;//利用向量统计待排序数字出现的个数
}
for (int i = 0,j = 0; i < vec_size && j < len; i++)
{
if (vec[i] == 0) continue;
for (int k = 0; k < vec[i]; k++)
{
arr[j++] = i;
}
}
free(vec);
}
3.算法分析
时间复杂度: 排序数组大小为n,统计数组大小为k,将待排序数字插入统计数组需要时间o(n),遍历一遍统计数组所需时间为o(k),则最好、最坏和平均时间复杂度都为o(n+k)。
空间复杂度: 所需要分配的空间为待排数组中数字的最大值加一。
稳定排序: 当数字相同时不会改变相对次序。
总结
基于散列表的桶排序、基数排序和计数排序通过以空间换时间的方式将时间复杂度进一步降到了o(n+k)。但是存在局限性,比如在桶排序和基数排序时数据分配极不平衡时,在一个桶中就会出现很长的链表,散列表的查找变成了链表的查找,排序算法就会退化成基于线性表比较的排序算法。当数据的偏差很大时,计数排序就要分配很大的空间,而大部分空间都是没有用的。