目录
相信大家一看到标题就知道这篇博客要写什么,没错,就是C语言中的函数---qsort
我们学习一个函数,首先我们得先认识它,知道它的作用是什么,不然我们的学习兴趣就会失去一半甚至更多
认识到理解使用qsort函数
第一步:认识qsort函数
qsort全称quick sort,意为快速排序,所以我们很容易联想到这是一个关于排序的函数,好接下来我们来观察一段代码
#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于比较两个整数的大小
int compare(const void *a, const void *b)
{
return (*(int*)a - *(int*)b);
}
int main() {
int arr[] = {5, 3, 8, 1, 2};
int n = sizeof(arr) / sizeof(arr[0]);
//调用qsort函数
qsort(arr, n, sizeof(arr[0]), compare);
//打印数组
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
通过运行结果我们就能发现这段代码确确实实的实现了排序的功能,接下来让我们一起分析这段代码
代码分析:
从上面这段代码中我们能看到,代码开头定义了一个整形数组并进行赋值{5,3,8,1,2},并且运用sizeof计算出了数组长度存放在了n中,紧接着下一行代码就调用了qsort函数,观察它我们就会发现,我们给它传递了四个参数:arr,n,sizeof(int),compare(这里我们暂时不解释这些参数有什么作用)。然后进入compare函数内部,相信大家在一开头就能发现compare是一个我们自定义的函数,但是我们目前是不知道它有什么作用的,先跳过,回到主函数就会看到一个for循环,是用来打印排完序的数组,通过运行结果发现,代码确实实现了排序的功能。
第二步:了解特点
每个东西都有它自己的特点,函数当然也不例外,但是我们凭空想象或者观察上面的那段代码是不能得到qsort这个函数的特点的,那我们就可以使用比较法来观察,所以我们这里写出另一个排序函数用来比较
#include <stdio.h>
// 冒泡排序函数
void bubbleSort(int arr[], int n)
{
int i, j;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main()
{
int arr[] = {5, 3, 8, 1, 2};
int n = sizeof(arr) / sizeof(arr[0]);
// 调用冒泡排序函数
bubbleSort(arr, n);
// 输出排序后的数组
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
这是一段通过冒泡排序来实现排序的代码,通过观察运行结果可以发现,这两段代码都得到了相同的结果,也就是说均实现了排序的目的,所以我们来比较这两段代码。最容易发现的就是代码长短,冒泡排序比上我们的qsort函数多了十几行代码,所以
特点一:代码行长度短
继续观察冒泡排序,可以发现它之所以可以实现排序的功能,是我们自己通过循环、交换等一系列的方法来实现的,而我们的qsort函数,仅仅是进行了传参和编写一行函数compare函数就实现了排序功能,所以
特点二:现成的,使用方便
我们学习过冒泡排序就会发现,它通过内外循环来实现数据的比较和交换,其中外循环用来进行一趟内部的比较,内循环用来比较并且交换,然而关于for循环的特点,我们知道,在没有外界干扰的情况下,循环都需要进行完整,而qsort函数是基于一种快速的算法,运行速度快,所以
特点三:效率高
对于冒泡排序,一般只适用于一些简单的数据类型,如整数、浮点数等,而qsort函数则具有很高的通用性,就连比较复杂的结构体排序也能实现,所以
特点四:所有类型的排序都能实现
看到这里,相信大家已经充分的被qsort函数所吸引,并且迫不及待的想要理解它的原理并且运用它,所以接下来就让我们对它进行理解
第三步:理解qsort函数
在第一步认识函数中我们以及观察到,在使用qsort函数时需要给它传递四个参数,查阅资料我们发现使用qsort函数的基本形式为:qsort (void* base,size_t num,size_t size,int(*compar)(const void*,const void*))
我们对基本形式进行逐字理解
void* base-->是指针,指向被排序数组的第一个元素
size_t num-->被排序数组的元素个数
size_t size-->被排序数组的元素的大小(字节)
int(*compar)(const void*,const void*)-->compar函数指针,指针指向的函数用来比较被排序数组中的两个元素的比较
!!!这里的所有void*均是为了能接收所有类型的指针
有了这个认识,我们来解释第一步中调用qsort函数时四个参数的意义是什么
arr-->数组名,代表数组的第一个元素arr[0],作为起始地址
n-->元素个数5个
sizeof(arr)-->计算每个数组元素的大小(字节)
compare-->指向要比较的两个元素(如比较5,3),并有返回值。我们来重点理解compare函数
观察并分析函数:定义该函数时定义了两个由const修饰的指针变量a,b ,在函数内部,通过return直接返回了*(int*)a - *(int*)b
如果返回值<0,则表示第一个元素应排在第二个元素之前
如果返回值=0,则表示两个元素相等
如果返回值>0,则表示第一个元素应排在第二个元素之后
写compare函数
//我们知道compare函数是用来比较两个整型的大小的
//所以a指向了一个整型数据
//所以b指向了一个整型数据
int compare(const void* a,const void* b)
{
//我们在接收实参时使用了void*,使得a,b都是五类型的数据,所以我们需要将它们强制转换为整型
//所以(int*)的作用就是将a强制转换为整型,对b同理
//在前方加上*进行解引用操作(解引用的目的是为了获取指针所指向内存位置存储的实际整型数值,进而才能进行比较)
return (*(int*)a-*(int*)b);
//直接返回>0/<0/=0,简直明了(推荐)
}
通过以上三个步骤,我们已经初步认识并理解了这个函数,接下来我们将进行最令人激动的一步:实操!我们将用qsort函数对结构体排序来为我们进行更深入的理解
#include<stdio.h>
#include<stdlib.h>
//创建结构体变量
struct Student
{
char name[30];
int age;
};
//写比较函数
int compare(const void* a,const void* b)
{
return (*(struct Student*)a).age-(*(struct Student*)b).age;
}
void printarr(struct Student arr[],int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%s: %d\n",arr[i].name,arr[i].age);
}
}
void test()
{
struct Student arr[]= {{"zhangsan",20},{"lisi",18},{"wangwu",31}};
//计算数组长度
int sz=sizeof(arr)/sizeof(arr[0]);
//调用函数
qsort(arr,sz,sizeof(arr[0]),compare);
//打印数组
printarr(arr,sz);
}
//主函数
int main()
{
//排序函数
test();
return 0;
}
终于终于,我们到这里已经学习完了qsort函数,并且会使用它了,但是大家到这里,我相信大多数人心中一定有一个疑惑,那就是如果没有这个函数时,我们是怎么对结构体进行排序的,难道说通过我们的冒泡排序的方法真的不能进行结构体的排序吗?答案是显而易见的,我们是可以通过冒泡排序进行结构体排序的,并且不只是结构体,从而达到qsort函数的作用。
冒泡排序对qsort进行模拟实现
第一步:写主函数
int main()
{
test();
return 0;
}
主函数内通过定义test函数进行我们的代码测试
第二步:编写test函数
//这个函数不需要有返回值
void test()
{
struct student arr[]={{"zhangsan",20},{"lisi",18},{"wangwu",31}};
int sz=sizeof(arr)/sizeof(arr[0]);//计算数组长度
bubble_sort(arr,sz,sizeof(arr[0]),compare);//冒泡排序函数
printarr(arr,sz);//打印结构体
}
第三步:定义结构体
struct student
{
char name[30];//声明一个长度为30的字符数组name,用来存储姓名
int age;//用来存储年龄
};
第四步:编写冒泡排序函数
void bubble_sort(void* base,size_t sz,size_t width,int(*compare)(const void* a,const void* b))
{
int i=0;
for(i=0;i<sz-1;i++)//外部循环
{
int j=0;
for(j=0;j<sz-i-1;j++)//内部循环
{
if(compare((char*)base+j*width,(char*)base+(j+1)*width)>0)//判断是否交换
{
swap((char*)base+j*width,(char*)base+(j+1)*width,width);//实现交换
}
}
}
}
相信大家看到这儿就会感到迷惑了,为什么要这样定义函数,而这个函数的定义形式又和调用qsort函数时及其相似,这是为什么呢?首先我们再次明确我们在做什么,我们是对qsort函数进行模拟实现,达到同样的对所有类型的数据进行排序,而其他类型的数据并不能像整型数据一样直接交换,所以我们打起了地址的想法,我们都知道每一个数据都有相应的内存空间,也就是地址,所以我们就想如果把地址交换以后会不会实现数据的交换功能呢?为了得到这个答案,我们来观察下面这幅图片
在交换前如果我们打印ptr1和ptr2我们将会得到“hello world”,但是在交换后我们打印ptr1和ptr2我们将得到“world hello”。所以,通过这个,我们知道交换地址确实可以达到交换数据的目的
所以,回到这个函数的定义,由于我们已经明确了需要通过地址来实现对数据的交换,所以我们需要给函数传递地址(base就是首元素的地址),然后传递数组大小(sz),再传递元素大小(width),最后传递compare实现获得返回的正负,用来判断是否进行交换。到这里我们就知道了为什么这个冒泡排序需要这样进行定义。
第五步:编写compare函数
int compare(const void* a,const void* b)
{
return (((struct student*)a)->age-((struct student*)b)->age);
}
通过上文对qsort函数的理解,相信大家一定知道为什么这样写这个函数,所以我们这里重点不在这个函数上,而是放在传参上,为什么要为这个函数传递这样复杂的参数
看见这张图片,我们就来到了最难的部分,但是大家不用担心,我将用最容易理解的方式进行讲解
大家观察理解这张图片就应该知道为什么我们传参时传的是(char*)base+ j*width了,这样我们就得到了元素j的地址了。
第六步:编写交换swap函数
void swap(char* buf1,char* buf2,int width)
{
int i=0;
char t=0;//暂时存储第一个字节
for(i=0;i<width;i++)
{
t=*buf1;
*buf1=*buf2;
*buf2=t;
buf1++;
buf2++;//buf1和buf2同时++保证同时访问到j和j+1元素的下一个字节
}
}
在第六步中,我们已经得到了j和j+1的地址,但我们还不能直接进行交换,我们还要进行分割为一个一个字节,这也是为什么我们在前面为什么要用(char*)强制转换base的原因,因为char类型在C语言中具有特殊性,char*类型的指针在进行算术运算时,是以字节为单位进行移动
swap函数交换图文演示
相信聪明的各位看到这幅图一定可以明白字节的交换和移动
第七步:编写打印(printarr)函数
void printarr(struct student arr[],int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%s: %d\n",arr[i].name,arr[i].age);
}
}
全代码展示
#include<stdio.h>
#include<stdlib.h>
struct student
{
char name[30];
int age;
};
void swap(char* buf1,char* buf2,int width)
{
int i=0;
char t=0;
for(i=0;i<width;i++)
{
t=*buf1;
*buf1=*buf2;
*buf2=t;
buf1++;
buf2++;
}
}
int compare(const void* a,const void* b)
{
return (((struct student*)a)->age-((struct student*)b)->age);
}
void bubble_sort(void* base,size_t sz,size_t width,int(*compare)(const void* a,const void* b))
{
int i=0;
for(i=0;i<sz-1;i++)
{
int j=0;
for(j=0;j<sz-i-1;j++)
{
if(compare((char*)base+j*width,(char*)base+(j+1)*width)>0)
{
swap((char*)base+j*width,(char*)base+(j+1)*width,width);
}
}
}
}
void printarr(struct student arr[],int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%s: %d\n",arr[i].name,arr[i].age);
}
}
void test()
{
struct student arr[]={{"zhangsan",20},{"lisi",18},{"wangwu",31}};
int sz=sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr,sz,sizeof(arr[0]),compare);
printarr(arr,sz);
}
int main()
{
test();
return 0;
}
好了,到这里我们已经学完了qsort函数的所有知识,并用冒泡排序模拟实现了qsort函数,希望这些内容能给大家带来一些帮助。
最后,我想用一段话来结束:编程之道不在追求晦涩的奇技淫巧,而在于用最朴素的逻辑结构本质,正如达芬奇所言:“简单是终极的复杂。”愿你我都能成为代码世界的吟游诗人。