系列文章目录
学习系列文章:
【初识C语言】选择结构(if语句和switch语句)详细解答
【初识C语言】循环结构(while语句、do…while语句和for语句)详细解答
实战项目文章:
【初识C语言】经典扫雷C语言实战(原码+解析),看完就能上手拆解与修改
文章目录
- 系列文章目录
- 前言
- 一、qsort 函数是什么?
- 1、核心定位
- 2、函数原型(牢记4个参数!)
- 3、4 个参数通俗解读
- 二、qsort 的核心:自定义比较函数(重点中的重点!)
- 1. 比较函数通用规则
- 2.分类型写比较函数
- 场景 1:排序 int 类型(最基础,入门必学)
- 场景 2:排序 float/double 类型(避坑!不能直接返回差值)
- 场景 3:排序 char 类型(单个字符,按 ASCII 码排序)
- 场景 4:排序字符串类型(两种写法,重点区分!)
- 写法 1:二维 char 数组排序(常用,每行一个字符串)
- 写法 2:char * 指针数组排序(推荐,节省内存)
- 场景 5:排序结构体类型(进阶用法,面试高频)
- 写法 1:按结构体 int 成员排序(按年龄排序)
- 写法 2:按结构体字符串成员排序(按姓名字典序排序)
- 三、qsort 通用技巧 & 避坑总结
- 1. 通用技巧
- 2. 避坑指南(必看!)
- 总结
前言
小伙伴们!今天给大家分享一个 C 语言里的 “排序万能工具”——qsort函数。平时我们写排序,要么手写冒泡、快排,还得针对 int、char、结构体分别改代码,巨麻烦!而qsort函数能一键搞定所有数据类型的排序,高效又省心,这篇文章就带大家吃透它的含义和用法,希望对大家有用~
一、qsort 函数是什么?
1、核心定位
qsort是 C 语言标准库中的通用快速排序函数,定义在 <stdlib.h> 头文件中,支持对任意类型的数组进行排序(int、float、字符串、结构体统统不在话下),底层实现了快速排序算法,时间复杂度接近 O (NlogN),效率拉满!
2、函数原型(牢记4个参数!)
先看官方原型,对应我代码里的注释:
voidqsort(void*base,// 要排序的数组首地址(直接传数组名就行,比如arr_int)size_tnitems,// 数组元素个数(通用写法:sizeof(arr)/sizeof(arr[0]))size_tsize,// 单个元素的字节大小(比如sizeof(int)、sizeof(arr[0]))int(*compar)(constvoid*,constvoid*)// 自定义比较函数指针(核心!决定排序规则));3、4 个参数通俗解读
| 参数名 | 通俗含义 | 代码示例(int 数组) |
|---|---|---|
| base | 告诉 qsort “要排序的数组在哪”,数组名本身就是首地址,直接传即可 | arr_int(对应 int arr_int []) |
| nitems | 告诉 qsort “数组有多少个元素”,可以用 sizeof 计算,通用又方便 | sz = sizeof(arr_int)/sizeof(arr_int[0]) |
| size | 告诉 qsort “每个元素占多少内存”,推荐用sizeof(arr[0])(适配类型变更) | si = sizeof(int) 或 si = sizeof(arr_int[0]) |
| compar | 告诉 qsort “怎么比较两个元素”,需要我们自定义函数,决定是正序还是降序 | compar_int(自定义的 int 比较函数) |
二、qsort 的核心:自定义比较函数(重点中的重点!)
qsort之所以能适配所有数据类型,全靠这个比较函数。它有固定的格式要求,也有通用的排序规则,我们先掌握基础,再分类型拆解。
1. 比较函数通用规则
格式固定:返回值是int,两个参数都是const void*(通用指针,兼容所有类型,不能直接解引用,必须强制转换);
返回值含义(敲黑板!):这是排序的关键,记死这三句话:
- 返回负数:第一个参数对应的元素,排在第二个参数对应的元素前面(正序核心);
- 返回0:两个元素相等,顺序不变;
- 返回正数:第一个参数对应的元素,排在第二个参数对应的元素后面;
2.分类型写比较函数
下面结合我整理的代码,逐个讲解不同数据类型的比较函数写法,从简单到复杂,循序渐进~
场景 1:排序 int 类型(最基础,入门必学)
这是最简单的场景,比较函数直接返回两个数的差值即可,无精度丢失。
#include<stdio.h>#include<stdlib.h>// 自定义int类型比较函数(正序)intcompar_int(constvoid*a,constvoid*b){// 步骤1:const void* 强制转换为 const int*(还原int指针类型)// 步骤2:解引用,得到int类型的值// 步骤3:返回差值,满足比较函数返回规则return*(constint*)a-*(constint*)b;// 正序// return *(const int*)b - *(const int*)a; // 降序,颠倒a和b即可}intmain(){intarr_int[]={3,1,2,5,7,9,10,6,8,4};size_tsz=sizeof(arr_int)/sizeof(arr_int[0]);// 元素个数size_tsi=sizeof(arr_int[0]);// 单个元素大小inti=0;qsort(arr_int,sz,si,compar_int);// 调用qsort// 打印结果for(i=0;i<sz;i++){printf("%d ",arr_int[i]);}return0;}关键细节:const void* a不能直接解引用,必须先转为const int*,再用*解引用获取具体数值。
场景 2:排序 float/double 类型(避坑!不能直接返回差值)
很多新手会踩坑:直接返回 f1 - f2 ,但浮点型差值转 int 会截断小数(比如 1.25 和 1.26 的差值 0.01,转 int 后变成 0,qsort 无法判断大小),必须显式判断!
#include<stdio.h>#include<stdlib.h>intcompar_float(constvoid*a,constvoid*b){// 先转换并解引用,获取浮点值floatval_a=*(constfloat*)a;floatval_b=*(constfloat*)b;// 显式判断大小,返回1/-1/0,避免精度丢失if(val_a>val_b){return1;// a在后,正序}elseif(val_a<val_b){return-1;// a在前,正序}else{return0;// 相等}// 降序只需颠倒判断逻辑://if (val_b > val_a) {// return 1;//}//else if (val_b < val_a) {// return -1;//}//else return 0;}intmain(){floatarr_float[]={1.25f,1.26f,2.58f,9.48f,4.37f};size_tsz=sizeof(arr_float)/sizeof(arr_float[0]);size_tsf=sizeof(arr_float[0]);inti=0;qsort(arr_float,sz,sf,compar_float);for(i=0;i<sz;i++){printf("%.2f ",arr_float[i]);}return0;}避坑指南:double类型的比较函数和float完全一致,只需把类型换成double即可,同样不能直接返回差值!
场景 3:排序 char 类型(单个字符,按 ASCII 码排序)
char本质是 ASCII 码整数,比较逻辑和int一致,直接返回差值即可,还能实现不区分大小写排序(拓展用法)。
#include<stdio.h>#include<stdlib.h>#include<ctype.h>// tolower依赖头文件intcompar_char(constvoid*a,constvoid*b){// 正序:按ASCII码从小到大(A<Z<a<z)return*(constchar*)a-*(constchar*)b;// 不区分大小写排序:先转小写再比较// return tolower(*(const char*)a) - tolower(*(const char*)b);// 降序:return *(const char*)b - *(const char*)a;}intmain(){chararr_char[]={'b','A','r','E','P','z','a','o','t','W'};size_tsz=sizeof(arr_char)/sizeof(arr_char[0]);size_tsc=sizeof(arr_char[0]);inti=0;qsort(arr_char,sz,sc,compar_char);for(i=0;i<sz;i++){printf("%c ",arr_char[i]);}return0;}小知识:ASCII 码中,大写字母 < 小写字母,数字 < 字母。
场景 4:排序字符串类型(两种写法,重点区分!)
字符串排序是高频场景,分为「二维 char 数组」和「char * 指针数组」,两者的比较函数写法不同,千万别搞混!
写法 1:二维 char 数组排序(常用,每行一个字符串)
#include<stdio.h>#include<stdlib.h>#include<string.h>// strcmp依赖头文件intcompar_str_2d(constvoid*a,constvoid*b){// 直接转换为const char*,传入strcmp比较完整字符串returnstrcmp((constchar*)a,(constchar*)b);// 正序// return strcmp((const char*)b, (const char*)a); // 降序}intmain(){// 二维char数组:5行20列,每行存一个字符串chararr_str[5][20]={"hello","apple","tiger","banana","marry"};size_tsz=sizeof(arr_str)/sizeof(arr_str[0]);size_tsc=sizeof(arr_str[0]);// 单个元素是一行,大小为20inti=0;qsort(arr_str,sz,sc,compar_str_2d);for(i=0;i<sz;i++){printf("%s ",arr_str[i]);}return0;}关键细节:a/b直接指向每行字符串的首地址,转换为const char*后可直接传入strcmp(strcmp专门比较字符串,返回值符合 qsort 要求)。
写法 2:char * 指针数组排序(推荐,节省内存)
#include<stdio.h>#include<stdlib.h>#include<string.h>intcompar_str_ptr(constvoid*a,constvoid*b){// 步骤1:转换为const char**(指向char*指针的指针)// 步骤2:解引用,得到const char*(字符串首地址)// 步骤3:用strcmp比较returnstrcmp(*(constchar**)a,*(constchar**)b);// 正序// return strcmp(*(const char**)b, *(const char**)a); // 降序}intmain(){// char*指针数组:存储字符串常量的首地址char*arr_str[]={"hello","apple","tiger","banana","marry"};size_tsz=sizeof(arr_str)/sizeof(arr_str[0]);size_tsc=sizeof(arr_str[0]);// 单个元素是char*指针,大小4/8字节inti=0;qsort(arr_str,sz,sc,compar_str_ptr);for(i=0;i<sz;i++){printf("%s ",arr_str[i]);// arr_str[i] 等价于 *(arr_str + i)}return0;}关键细节:a/b指向的是char*指针(数组元素),因此必须先转为const char**,解引用后才能得到字符串首地址,传入strcmp。
场景 5:排序结构体类型(进阶用法,面试高频)
结构体排序需按指定成员排序,分为「int 成员」和「字符串成员」,写法略有差异,但核心不变。
写法 1:按结构体 int 成员排序(按年龄排序)
#include<stdio.h>#include<stdlib.h>// 定义学生结构体structstudent{charname[20];intage;};intcompar_stu_age(constvoid*a,constvoid*b){// 两种等价写法,任选其一// 写法1:先解引用指针,再用.访问成员return(*(conststructstudent*)a).age-(*(conststructstudent*)b).age;// 写法2:先转换为指针,再用->访问成员(更简洁)// return ((const struct student*)a)->age - ((const struct student*)b)->age;}intmain(){structstudentarr_stu[]={{"zhangsan",18},{"lisi",20},{"wangwu",21},{"zhaoliu",17}};size_tst=sizeof(arr_stu)/sizeof(arr_stu[0]);size_tsu=sizeof(arr_stu[0]);// 单个结构体大小inti=0;qsort(arr_stu,st,su,compar_stu_age);for(i=0;i<st;i++){// 两种等价访问方式printf("姓名:%s\t,年龄:%d\n",arr_stu[i].name,arr_stu[i].age);// printf("姓名:%s\t,年龄:%d\n", (&arr_stu[i])->name, (&arr_stu[i])->age);}return0;}关键细节:( * (struct student * )a).age 等价于 ((struct student * )a)->age,后者更简洁,推荐使用;正序 / 降序同样只需颠倒差值顺序(即改变a/b位置)。
写法 2:按结构体字符串成员排序(按姓名字典序排序)
#include<stdio.h>#include<stdlib.h>#include<string.h>structstudent{charname[20];intage;};intcompar_stu_name(constvoid*a,constvoid*b){// 两种等价写法,任选其一returnstrcmp(((conststructstudent*)a)->name,((conststructstudent*)b)->name);// return strcmp((*(const struct student*)a).name, (*(const struct student*)b).name);}intmain(){structstudentarr_stu[]={{"Zhangsan",18},{"alice",20},{"wangwu",21},{"zhaoliu",17}};size_tst=sizeof(arr_stu)/sizeof(arr_stu[0]);size_tsu=sizeof(arr_stu[0]);inti=0;qsort(arr_stu,st,su,compar_stu_name);for(i=0;i<st;i++){printf("姓名:%s\t,年龄:%d\n",arr_stu[i].name,arr_stu[i].age);}return0;}关键细节:结构体字符串成员必须用strcmp比较,不能直接用==或差值比较!
三、qsort 通用技巧 & 避坑总结
1. 通用技巧
- 元素个数计算:用 sizeof(arr)/sizeof(arr[0]),无需手动修改,适配所有数组;
- 单个元素大小:优先用 sizeof(arr[0]),比直接写sizeof(int)更灵活,数组类型变更时无需修改;
- 访问方式等价性:
- arr[i] 等价于 *(arr + i)(数组下标与指针偏移);
- arr[i].member 等价于 (&arr[i])->member(结构体变量与指针的访问转换);
- (*p).member 等价于 p->member(结构体指针的两种访问方式)。
2. 避坑指南(必看!)
- float/double类型:绝对不能直接返回差值,必须显式判断大小,避免精度丢失;
- 字符串类型:必须用strcmp比较完整字符串,不能直接比较指针或单个字符;
- void*指针:不能直接解引用,必须先强制转换为对应类型,再解引用;
- 结构体访问:变量用.,指针用->,不能随意互换;
- 比较函数返回值:必须遵循 “负数在前、正数在后” 的规则,否则排序结果异常。
总结
qsort函数是 C 语言排序的 “万能钥匙”,核心在于自定义比较函数,只要掌握了不同数据类型的转换技巧和比较逻辑,就能轻松搞定所有排序场景。总结一下核心要点:
- qsort 4 个参数:首地址、元素个数、单个元素大小、比较函数;
- 比较函数:固定格式,返回值决定排序顺序,不同数据类型只需调整转换和比较逻辑;
- 高频场景:int(差值)、float(显式判断)、字符串(strcmp)、结构体(按成员比较);
- 通用技巧:优先用sizeof(arr[0]),牢记等价访问方式,避开精度丢失、类型不匹配的坑。
掌握 qsort,不仅能节省手写排序的时间,还能应对学习和面试中的各种排序场景,小伙伴们抓紧在编译器里跑一遍加深理解吧~