(1) qsort 函数介绍和使用
在 C 语言中,有一些题目会经常出现,比如说,总是会让你对一个整形数组进行排序,有与之对应的各种排序算法,什么插入,快排... 诸如此类的,那问题来了,能不能用一个函数,实现我可以对任意类型数据进行排序.
其实可以的,接下来我们就来解析一下qsort 这个函数,这个函数,我们在: cplusplus.com/reference/cstdlib/qsort/
这个网址中能找到qsort 的详细说明,我直接截图过来
这是这个函数的参数以及解释,我先把解释翻译一遍: 排序被 base 指针指向的数组中num个元素,其中每个元素的长度是 size 个字节,用 compar 指针所指向的函数作为排序的判断条件 (当然其中的 base num size compar 都是函数的参数) (头文件: <stdlib.h> )
有的小伙伴还是没明白啥意思,不急,我们先看看官方对这个函数各个参数的解释:
首先第一个 void* base 这是一个泛型指针:
它给出的对于这个泛型指针的用途的解释是: 这个指针指向的是待排序数组的第一个元素. 也就是说,我把待排序数组的第一个元素的地址(数组名) 传给了base
再下来第二个参数: num
这个就比较简单了,这个 num 表示的就是base 所指向的数组中待排序元素的个数,不过我们一般是整个数组都要排序,所以大多数时候可以简单把它理解成整个数组的元素个数.(因为元素个数没有负数,所以类型是 size_t )
其次是第三个参数: size
size 是数组中每个元素的长度,单位是字节
大家想一想,既然我并不知道base指向的空间里面放的是什么类型的元素,我是不是就需要知道元素个数和一个元素多长,这样才能对这块内存的布局和分配有一个清晰的认识,后期才好交换和分析
对不?
其次第四个参数,也是最关键的一个参数: int (*compar)(const void * , const void*) 这是一个函数指针,表示它指向的函数的返回类型为int 并且这个函数的两个参数都是const void* 对于它的解释为:
他这后面的意思就是compar指针所指向这个函数的返回值是根据其两个参数的大小关系来确定的,假如说它所指向的函数的两个参数是
const void* e1 和 const void* e2 当 *e1 > *e2 返回大于0 的数 当 *e1< *e2 返回小于0 的数,当 *e1 == *e2 时, 返回 0
也就是说这个函数是需要自己写的,它的参数和返回类型以及实现逻辑都是规定好的,当你写好这个函数时,在qsort 的第四个实参上传函数名即可
有的小伙伴还是不理解第四个参数的意思,那我就先浅浅给大家分析一下,等一下模拟实现的时候在跟大家细讲~
qsort 想要实现任意类型数据的排序,那么数据之间比较的方法就不能写死,比如,我今天要排序一个整形数组中的数据, 那用 > 和 < 比较再合适不过了,那如果我要排序一个字符数组中的数据呢?
用 > 和 < 恐怕不能解决问题吧? 而且,对于同一个数组来说,比较的方式也不是唯一的,比如对于结构体的数组,我们知道,结构体类型是专门来描述复杂对象的,比如描述一个学生, 她/他 有性别,有年龄,有身高等等,那把这样多个结构体放在一个数组中进行排序时,排序依据是啥? 是姓名,年龄,性别还是其他的啥?
我想这个依据对于不同的人可能不一样吧? 所以就更得自己写了. 你自己想怎么排序,你自己怎么写,完了你把这个自己排序函数传给qsort qsort根据你的排序函数来对这个数组中待排序的元素进行排序不就完了嘛?
那有的小伙伴就说了,你说了这么多我都没看见,那咱们直接上例子吧,先整个简单的,对一组整形元素进行排序:
#include <stdlib.h>
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void Print_Arr(int arr[], size_t num)
{
for (int i = 0; i < num; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
size_t num = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
Print_Arr(arr, num);
qsort(arr, num, sizeof(arr[0]), cmp_int);
printf("排序后:");
Print_Arr(arr, num);
return 0;
}
大家看,首先,我在数组里面放上10个乱序的整形 然后我利用qsort函数排序,按照函数格式依次给它传首元素地址,元素个数,一个元素的长度以及排序函数,这里我提供了一个Printf_Arr 函数 用来打印数组的元素,以便对排序前后的数组进行对比,来看一下结果:
大家看, 我是不是用 qsort 函数 把 arr 数组排成了升序呀
那有的小伙伴就问了 qsort 是不是默认把元素排成升序? 是的,那我怎样让qsort 把元素排成降序呢? 大家结合它对排序函数的返回值的要求和我举的这个排序例子可以猜测一下, 当 *e1 > *e2 时返回值大于0 当*e1 == *e2 的时返回值等于0 当*e1 < *e2 时 返回值小于0 而大家想,在我这个例子中,既然qsort 默认给它排成升序,那么是不是当前面大于后面的时候排序呀,而你看,前面大于后面时返回值大于0 也就是说当返回值大于0 的时候qsort 会进行排序,而其他两种情况却不会.
结论: qsort 进行排序的条件是: 排序函数的返回值 > 0 ,其中的 e1 指向了前面一个元素, e2 指向了后面一个元素
因此,在未来,你想把一组数据排成 升序 / 降序 , 那么在写排序函数只需要把 升序 / 降序 相对应的交换逻辑的返回值设为 > 0 的数就行了
大家想一想这样是不是就可以实现对任意类型数据的排序
如果听懂的话,就来看我下面这两个例子,如果你看懂了,那么说明 qsort 的使用你就差不多了
#include <string.h>
struct Stu {
char name[20];
int age;
int sorce;
};
int cmp_Stu(const void* e1, const void* e2)
{
return strcmp((*(struct Stu*)e1).name, (*(struct Stu*)e2).name); //按名字字符串升序排列
}
int main()
{
struct Stu arr[3] = { {"zhangsan",15,98},{"lisi",12,81},{"wangwu",20,100} };
size_t num = sizeof(arr) / sizeof(arr[0]);
qsort(arr, num, sizeof(arr[0]), cmp_Stu);
return 0;
运行结果(通过调试观察) :
如果我把排序函数内部设成:
int cmp_Stu(const void* e1, const void* e2)
{
// return strcmp((*(struct Stu*)e1).name, (*(struct Stu*)e2).name); //按名字字符串升序排列
return (*(struct Stu*)e2).age - (*(struct Stu*)e1).age; // 按年龄的降序排序
}
调试观察运行结果:
你学会了嘛~
(2) qsort 以冒泡排序模拟实现
① 冒泡排序整形
所谓冒泡排序,就是在一组乱序的数组元素中两两相邻元素进行比较,不满足顺序就交换,例如我要把一组整形元素排成升序
关于这个排序算法,小伙伴们可以参考: C/C++排序算法(三)—— 冒泡排序和快速排序_c++ 必冒泡更快的-CSDN博客
这里面的动态图做的不错,相信看完之后你就能理解了
② 用冒泡排序实现qsort
接下来我们就来改造一下冒泡排序,让它可以排序任意类型的数据,我还是把排序整形的源码拿一份过来
void Print_Arr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void bubble_sort(int arr[], int sz)
{
for (int i = 0; i < sz - 1; i++)
{
int flag = 1;
for (int j = 0; j < sz-1-i ; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tmp;
flag = 0;
}
}
if (flag)
break;
}
}
int main()
{
int arr[10] = { 9,4,3,6,8,5,2,0,1,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
Print_Arr(arr, sz);
bubble_sort(arr, sz);
printf("排序后:");
Print_Arr(arr, sz);
return 0;
}
大家看,我现在这个冒泡排序函数只能排序整形,那我怎样让它排序任意类型的数据呢?
首先,看我上面传数组名的时候,形参用的是整形数组来接收(其实质就是一个整形指针),那问题来了,未来我想实现排序任意类型的数据,那我怎么知道你要排序的数组中是什么样的数据呢? 那你传过来数组名我应该拿什么类型的指针接收? char* , int* 还是 struct S* ? 所以我不知道你要传过来什么类型数据的地址, 那我就只好用 void* 它可以接收任意类型数据的地址,这样是不是才和我对任意类型数据进行排序的意图相吻合?
所以bubble_sort 的第一个参数 不应该是整形指针,而应该是 void* base 用来指向任意数据类型的一块空间.
好啦,我们知道,void* 的指针不能解引用,也不能进行指针运算,原因是 void* 表示的是无具体类型的指针, 所以你传过来地址的时候,我的 base
并不知道这个地址处放的是什么类型的数据,只是单纯的指向以这个数据为首的一块内存空间而已,也就是说,我现在对这块空间的分配情况根本就是一抓瞎,啥都不知道,就像你说: 沿着鼠标光标移动的方向走 你也没说走几个空格,你也不说走到什么地方停止,那我怎么知道你想表达什么意思
所以,bubble_sort 的第二个和第三个参数就应运而生了,第二个参数是元素个数 num, 第三个参数是 size 即一个元素的长度,这个时候就像在告诉你说,你往前走,不要回头,上天让你错过谁都有理由,别怕.... (额,不好意思串台了)
这个时候就像在告诉 base 说,你往前走, 走 num 次,每次走 size 个字节,这样我是不是对这块内存空间的分配有个清晰的认知,方便以后元素的比较和交换,不知道小伙伴们能否理解~
这个时候还差点意思啊,我知道你的这块空间是怎么存放数组元素了,那两两元素怎么比较? 那这样吧,我第四个参数设一个函数指针,你把你写的两两元素比较的函数传过来,我直接用你的函数帮你比较不就行了?
经过分析,我们发现,想要实现任意类型数据的排序,参数还得和 qsort 一样.
接下来,我就把这个bubble_sort 函数改造成能够排序任意类型的数据
//冒泡模拟实现qsort
void Swap(char* u1, char* u2, size_t sz)
{
for (int i = 0; i < sz; i++)
{
char tmp = *u1;
*u1 = *u2;
*u2 = tmp;
u1++;
u2++;
}
}
void bubble_sort(void* base,size_t num,size_t sz,int (*compar)(const void*,const void*))
{
for (int i = 0; i < num - 1; i++)
{
for (int j = 0; j < num-1-i ; j++)
{
if (compar((char*)base + (j * sz), (char*)base + ((j + 1) * sz))>0)
{
Swap((char*)base + (j * sz), (char*)base + ((j + 1) * sz),sz);
}
}
}
}
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[10] = { 9,4,3,6,8,5,2,0,1,7 };
size_t num = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, num, sizeof(arr[0]), cmp_int);
return 0;
}
来看一下,不管我排序啥样的数据,一趟冒泡排序搞定一个最大值,只要元素个数不变的情况下,我的趟数永远都是 元素个数 - 1 对吧, 而我每一趟内部所要比较的元素的对数也不变吧,也就是说我虽然是不同类型的数据,但是我排序的思想还是不变的对不?然后我两两相邻元素比较的方式由我自己定吧,这个地方不知道有没有小伙伴看懂我是怎么比较两个元素的,原理是:在冒泡排序的思想中,在 if 语句的判断条件里是不是要 把
arr [ j ] 和 arr [ j+1 ] 作比较,有必要的话交换对吧,那我作比较肯定是用 cmpar 所指向的,也就是我自己写的比较函数来比较,我们来看一下这个比较函数的格式:
int cmp (const void* , const void* )
这里我就暂且把它命名为 cmp , 它的两个参数类型都是 const void* 现在回头想一想,为啥呀? 因为我也不知道这两个地址处放的是什么类型的元素,所以我是 void* 我也不希望这个地址处所存放的元素被修改,所以我是 const .
接下来我假设一个元素地址是 e1 另一个元素地址是 e2 因为上面的代码中写的还是针对整形元素的,所以我直接返回一个 *e1 - *e2 ,并在结果大于0,也就是前面比后面大的时候进入 if 交换位置,那这地方我是怎么表示 arr [ j ] 和 arr[ j + 1 ] 的地址的?
有人说你直接一个 base + j 和 base + j + 1不就完了吗? 问题是我的 base + j 会跑到哪里去不知道呀, 我 base 的类型是void* 呀, 泛型指针 + - 整数 的语法是错误的呀 ,因为我的指针类型决定 + - 整数时移动几个字节,而我的泛型指针是无具体类型的指针, + - 整数时,不知道应该跳过几个字节,所以 + - 整数的操作是不被允许的, 那我咋办,强转呗,强转成啥类型呀,如果是强转成 ( int * ) base 那我的 base + j 就相当于从 base 向后跳过了 4 * j 个字节,那我怎么知道这个是不是下标为 j 元素的地址呢? 所以这样写显然不行,那咋写? 我们不是传了一个参数表示每个元素几个字节吗? 这个时候就派上用场了, 既然一个元素是 sz 个字节, 那我 j * sz 是不是就是 j 个元素的总长度, 那我 base + j *sz 是不是就从 base 跳过 j 个元素,而我的下标是从 0 开始的,这时候是不是就正好指向了下标为 j 元素的地址呀,前提是你得先把base 强转成 char* 才行 不然就跳得比当前位置远多了,对于 下标为 j + 1 元素的地址也是一样分析的.
然后比较完满足条件我进去交换吧, 有的小伙伴就说了,那我自己写一个交换函数,就仿照你的比较函数的格式,然后你再设一个函数指针,接收我这个交换函数不就行了吗? 这想法确实可行,唯一的缺陷是,你这样搞的话,那元素咋比较是你写的,元素咋交换也是你写的,那你这不就是自己排序了吗? 而我们是要让 bubble_sort 帮我们排序呀,所以得让 bubble_sort 在函数内部完成元素交换,那咋交换,你还是把我要交换的两个地址给我Swap
我帮你交换嘛,那这地方站在这两个地址的角度,由于不知道怎么交换,所以我传了一个元素长度过去,两个元素一样长,把它们划分成很多小单位一个字节,然后我这两个数据一个一个字节的交换不就行了吗? 也就是:
在这个过程中,每个元素有几个字节就要交换几次,然后交换完一对后 u1 和 u2 都往后一步,交换下一对,直到走完不就行了吗?
小伙伴们可以对着我上面代码的逻辑一步步理解~
好啦,这一篇就到这里,我们下一篇再见~