@[TOC](文章目录)
引言
我们在写一些代码的时候经常会需要排序一个数组,我们可以通过一个经典的冒泡排序来实现。
冒泡排序
下面我们来简单的了解一下冒泡排序:假如我们想要将一个乱序的数组nums升序排列
int nums[10] = { 9, 7, 8, 6, 4, 5, 3, 2, 1, 0 };
冒泡排序的思路是将一个数 nums[i] 与它后面的数进行比较,如果 nums[i] 大于 nums[i+1],就将这两个数互换。在用这样的方式遍历一遍数组后,数组的最后一个元素一定是整个数组中最大的元素。这样的一趟能够将最大的数移动到数组的末尾,接下来我们需要再将剩余元素中的最大值用同样的方式确认出来放置在倒数第二个位置。依此类推,每趟都找到剩余元素中的最大值,即可升序排序整个数组。图示:
不难发现,在第一趟中两两交换,想要将最大的数9移到数组末端需要numsSize-1次交换(numsSize是数组的长度);第二趟需要numsSize-1-1次交换;第三趟需要numsSize-1-2次交换。依次类推,每一趟需要numsSize-1-i次交换,i的值从0开始递增1。而当第九趟结束后,我们已经排序好了9个元素,这时最后一个元素就不需要再进行第十趟了,所以排序numsSize个元素只需要经过numsSize-1趟。接下来我们实现冒泡排序:
#include<stdio.h>
void bubble_sort(int* nums, int numsSize)
{
int i = 0;
int j = 0;
for (i = 0; i < numsSize - 1; i++)
{
for (j = 0; j < numsSize - 1 - i; j++)
{
if (nums[j]>nums[j + 1])
{
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
int main()
{
int nums[] = { 9, 7, 8, 6, 4, 5, 3, 2, 1, 0 };
int numsSize = sizeof(nums) / sizeof(nums[0]);
bubble_sort(nums, numsSize);
int i = 0;
for (i = 0; i < numsSize; i++)
{
printf("%d ", nums[i]);
}
return 0;
}
i表示趟数,一共numsSize-1趟;j表示每一趟的个数,每一趟numsSize-1-i次交换。当判断nums[j]>nums[j+1]后将这两个数字交换。这样就可以实现数字的排序。
同时,我们可以对这段代码稍作改进。当这个nums数组本来就是升序排列时,或者当某趟交换后就完成了排序,就不需要继续遍历了,这时及时的跳出循环可以使排序更高效。为了实现这个目的,我们定义一个变量x用来判断。在每一趟循环之前令x = 1,如果if成立进入交换时就给x赋值为0。在后面对x进行检验,如果x为0就表示这一趟发生了交换,即还没有完成排序就不进入if;如果x不是0就表示这一趟没有发生交换,即已经完成了循环,这时进入if执行break跳出循环。代码如下:
void bubble_sort(int* nums, int numsSize)
{
int i = 0;
int j = 0;
int x = 0;
for (i = 0; i < numsSize - 1; i++)
{
x = 1;
for (j = 0; j < numsSize - 1 - i; j++)
{
if (nums[j]>nums[j + 1])
{
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
x = 0;
}
}
if (x)
{
break;
}
}
}
int main()
{
int nums[] = { 9, 7, 8, 6, 4, 5, 3, 2, 1, 0 };
int numsSize = sizeof(nums) / sizeof(nums[0]);
bubble_sort(nums, numsSize);
int i = 0;
for (i = 0; i < numsSize; i++)
{
printf("%d ", nums[i]);
}
return 0;
}
但是,这样的冒泡排序只能对整型数组进行排序而不能排序其他类型如字符串、结构体、浮点型。这时,库函数qsort实现了对于任何东西的排序。
接下来就假设我们需要给一组结构体类型的数组排序:
struct stu
{
char name[20];
int age;
};
struct stu s[3] = { { "zhangsan", 24 }, { "lisi", 23 }, { "dashabei", 18 } };
库函数qsort
这个库函数被声明在头文件stdlib.h中,我们可以查询到这个库函数的声明:
函数声明的解析
返回值
qsort函数的返回值时void也就是空返回值。
参数
qsort函数有四个参数:
base
参数base的类型是void*(空类型的指针),这个类型的指针可以接收任何类型的指针。但是void*型的指针不能解引用访问其所指向的元素,想要解引用就必须将其强转为其他类型的指针。
base接收要排序的数组的首元素地址。
num
参数num的类型是size_t(无符号整型)。
num接收要排序数组的元素个数。
size
参数size的类型也是size_t。
size接收要排序数组每个元素的大小(字节数)。
cmp
参数cmp的类型是一个函数指针,在前面已经了解过函数指针的定义这里再简单提一下:
函数指针就是指向函数的指针(它和函数名的含义一致,所以我们在用函数指针调用函数时不用解引用*;在把函数指针传给一个函数指针变量时不用取地址&)。对于cmp这个函数指针指向的函数,它的参数是两个const void*型的指针(用const再*前修饰指针变量使指针指向的内容不能被改变),返回值是int型的。
cmp接收一个函数(函数指针),这个函数需要我们自己实现,作用是比较两个元素并返回比较结果。
cmp函数的实现
由于这个函数并不知道我们要排序的东西是什么,所以需要我们通过自己实现数组内两个元素的比较,并通过int返回比较的结果:当前一个元素大于后一个元素时返回一个大于0的数、小于时返回一个小于0的数、等于时返回0。并且这个cmp函数的两个参数事先只能被设置为void*以接收任意类型的指针,再用const修饰使其指向的内容不能被改变。
假设我们想要按照结构体中name的首字母顺序来排序,现在,我们来尝试实现一下cmp函数:
首先我们将这个函数的函数名确定为cmp_name。
int cmp_name(const void* e1, const void* e2)
接下来实现函数主体:这个函数的两个参数都是const void* 的,所以我们再比较时需要将这两个指针的类型还原为原本的指针类型即结构体指针:
(struct stu*)e1
(struct stu*)e2
接下来需要通过这两个结构体指针找到其中的结构体成员。之前提到过->操作符可以通过结构体指针访问结构体成员:
((struct stu*)e1)->name
((struct stu*)e2)->name
这样,就得到了两个字符串。但是,两个字符串的大小不能直接比较,我们需要借助strcmp函数。这是一个库函数,包含在string.h头文件里,作用是比较两个字符串的大小并返回结果(当前一个字符串大于后一个字符串时返回一个大于0的数、小于时返回一个小于0的数、等于时返回0)。运用这个库函数,我们就可以直接实现cmp_name函数的返回值:
int cmp_name(const void* e1, const void* e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
接下来只需要再在调用qsort函数时将其作为实参即可。
qsort的调用
我们想要按照name排序结构体数组s中的元素。刚才已经解决了cmp参数,接下来就要实现另外三个实参:
base是数组的首元素地址。数组名是首元素地址,所以我们直接以数组名作为形参即可。
num是数组元素个数。我们可以通过sizeof(s)/sizeof(s[0])获得。当然这里的数组大小是3,直接用常量3作为元素个数即可。
size是每个元素的大小。我们可以通过sizeof(s[0])获得。
到此,我们就实现了这个函数的调用:
qsort(s, 3, sizeof(s[0]), cmp_name);
用qsort排序结构体
经过了上面的学习,我们只需要实现main函数的主体就可以实现用qsort排序结构体了:
#include<string.h>
#include<stdlib.h>
struct stu
{
char name[20];
int age;
};
int cmp_name(const void* e1, const void* e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int main()
{
struct stu s[3] = { { "zhangsan", 24 }, { "lisi", 23 }, { "dashabei", 18 } };
qsort(s, 3, sizeof(s[0]), cmp_name);
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s\n", s[i].name);//用.通过结构体变量访问结构体成员
}
return 0;
}
for循环打印的结果就是按name排序的元素了。
用冒泡排序的原理实现bubble_qsort
为了更好的理解qsort函数的作用原理我们用冒泡排序的方式来实现一个bubble_qsort(库函数的qsort内部的实现是以快排的方式先暂时不提):
我们需要实现的是,这个bubble_qsort和库函数qsort一样可以在函数不知道要排序的元素类型的情况下完成对任意类型元素的排序。在这过程中,我们可以更好地理解qsort的各个参数。
bubble_qsort函数框架
首先写出函数名、返回值、参数与qsort函数相同。
我们使用上面已经实现的冒泡排序来作为主要框架:
void bubble_qsort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
int x = 0;
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
x = 1;
if ()//调用cmp函数进行判断
{}//交换base[j]与base[j+1]
}
if (x)
{
break;
}
}
}
cmp函数的调用(回调函数)
在if判断部分,我们需要取到数组中第j个元素和第j+1个元素的地址再交给cmp函数进行判断。
在前面我们已经了解过回调函数的定义:就是被另一方通过函数指针调用的函数。这里的cmp函数就是一个回调函数,它的指针(也就是函数)被作为参数传给qsort函数(当然这里是bubble_qsort),在qsort函数中被调用。
因为函数并不确定我们想要排序什么类型的元素,所以这个函数调用的实参不能写成某个固定的类型。再结合cmp接收这两个指针时用的是void*型的,并且由于用户在实现cmp函数时会将这个void*的指针强转回他想要比较的元素类型。现在要解决的问题就是将第j个元素和第j+1个元素的地址精准的找到即可传递。
前面我们讲过,指针类型决定了指针加减整数时所移动的字节数。最小的char*类型的指针一次只能移动一个字节。我们只需要将这个base指针强转为char*型,再结合每个元素的大小(参数size)即可找到第j个元素和第j+1个元素:
((char*)base) + j*size//第j个元素的地址
((char*)base) + (j + 1)*size//第j+1个元素的地址
由此,我们就可以将cmp的调用放在if语句的判断部分:
if (cmp(((char*)base) + j*size, ((char*)base) + (j + 1)*size)>0)
当cmp的返回值大于0时说明第j个元素大于第j+1个元素,进入if语句进行交换。
swap函数的定义与调用
接下来,在进入if语句后,我们需要将第j个元素与第j+1个元素互换。我们不妨封装一个swap函数用来交换这两个变量。
在定义函数时,要实现两个元素的交换首先就需要获得这两个元素的指针。对于两个元素的指针,我们像cmp函数那样用char*型的指针作为参数即可。由于不知道要排序的类型,我们也无法设置一个固定的中间变量temp来直接交换两个元素的位置。但是,结合每个元素的大小(参数size)我们就可以将temp设置为char然后逐字节的交换两个元素。这就需要我们再将size作为swap函数的参数以确定逐字节交换的次数:
void swap(char* left, char* right, int size)
{
int i = 0;
char temp = 0;
for (i = 0; i < size; i++)
{
temp = *(left + i);
*(left + i) = *(right + i);
*(right + i) = temp;
}
}
将两个元素的地址以char*类型接收。for循环交换两个起始指针后的第i个字节的内容,i<size。
在调用这个swap函数时,只需要像cmp函数调用时一样传递两个char*指针即可,并且将size传递给swap:
swap(((char*)base) + j*size, ((char*)base) + (j + 1)*size, size);
最后展示一下bubble_qsort函数的实现:
void swap(char* left, char* right, int size)
{
int i = 0;
char temp = 0;
for (i = 0; i < size; i++)
{
temp = *(left + i);
*(left + i) = *(right + i);
*(right + i) = temp;
}
}
void bubble_qsort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
int x = 0;
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
x = 1;
if (cmp(((char*)base) + j*size, ((char*)base) + (j + 1)*size)>0)
{
swap(((char*)base) + j*size, ((char*)base) + (j + 1)*size, size);
x = 0;
}
}
if (x)
{
break;
}
}
}
bubble_qsort函数排序结构体
上面我们完成了用qsort函数对s按照name排序,在实现了bubble_qsort函数之后,我们不妨用它来按照age排序一下上面的那个结构体数组s:
这时函数cmp的实现就是需要完成两个int类型变量的比较,所以返回值可以直接写成两个int数据的相减:
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
再完善主函数部分,就有了如下的代码:
#include<string.h>
#include<stdlib.h>
struct stu
{
char name[20];
int age;
};
int cmp_age(const void* e1, const void* e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
void swap(char* left, char* right, int size)
{
int i = 0;
char temp = 0;
for (i = 0; i < size; i++)
{
temp = *(left + i);
*(left + i) = *(right + i);
*(right + i) = temp;
}
}
void bubble_qsort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
int x = 0;
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
x = 1;
if (cmp(((char*)base) + j*size, ((char*)base) + (j + 1)*size)>0)
{
swap(((char*)base) + j*size, ((char*)base) + (j + 1)*size, size);
x = 0;
}
}
if (x)
{
break;
}
}
}
int main()
{
struct stu s[3] = { { "zhangsan", 24 }, { "lisi", 23 }, { "dashabei", 39 } };
bubble_qsort(s, 3, sizeof(s[0]), cmp_age);
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s\n", s[i].name);
}
return 0;
}
总结
在本文中,我们了解了冒泡排序的实现、库函数qsort的使用、bubble_qsort函数的实现。
通过bubble_qsort的实现,我们对qsort函数的参数理解更加深刻,在以后使用qsort函数时也会更加的得心应手。
如果对本文有任何问题,欢迎在评论区进行讨论哦
希望与各位共同进步!!!