C语言库函数之 qsort 讲解、使用及模拟实现
引入
我们在学习排序的时候,第一个接触到的应该都是冒泡排序,我们先来复习一下冒泡排序的代码,来作为一个铺垫和引入。
代码如下:
#include<stdio.h>void bubble_sort(int *arr, int sz)
{int i = 0;for (i = 0; i < sz - 1; i++){int j = 0;for (j = 0; j < sz - 1 - i; j++){if (arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}
很简单的一种排序方法,但我们可以发现一个问题,那就是冒泡排序不够通用,它只能用于整型数组的排序,如果我要排序float类型,或者排序结构体要怎么办呢。
下面,我们就来介绍一个比较万能的排序函数,qsort函数
简介
先来简单了解一下qsort函数的各个部分
语法格式
它的固定格式如下:
int cmp_int(const void* e1, const void* e2)
{}
注意:他的格式是固定的,比如返回值类型必须是int类型,形参的类型也是固定的,只有返回值的部分是自己编写的,也就是说,当返回值类型不是int的时候,我们需要进行强制类型转换或者手动将其返回值改成int类型
(这在下文会详细说明)
参数解释
在调用函数时,传参格式如下:
void qsort(void* base,size_t num,size_t width,int (*cmp)(const void* e1, const* e2)
);
可以看一下这张图,里面讲解了qsort函数的各个参数分别表示的是什么,
base:起始位置,待排序数组的首元素地址 num:数组的大小,单位是元素,待排序数组的元素个数
width:元素大小,单位是字节,待排序数组的单个元素的大小 cmp:函数指针(比较函数:compare
function),比较两个元素的函数的地址
解释:对于不同类型元素的比较的方法是不同的,此处就是将两个元素的比较方法写成函数,传到qsort函数中,然后使用指针cmp进行调用
e1和e2可以简单地认为是要比较的两个元素的地址,(下面会做补充说明)
对void *的解释
先抛出一个问题:下面这个代码有什么问题
int main()
{int a = 0;int *pa = &a;char* pc = &a; return 0;
}
问题就是:第四行和第五行:此处虽然可以存储,但会报警告:从“int *”
到“char *”的类型不兼容。
那么,我们这时就可以使用,void *(无指针类型)来解决这个问题
void* p = &a;
//void *类型的指针可以接收任意类型的地址
此处就可以很好地解释qsort函数的第一个参数:void* base
补充:
对于void *类型的指针无法进行解引用
因为不知道进行解引用之后,要访问几个字节
同理,也无法进行无符号型的指针与整数的运算
那么,在qsort函数中,如何比较e1与e2呢
可以将二者强制转换成所需的类型(代码中会提到这一点)
返回值
下图是英文版的介绍
对qsort函数返回值的解释
当e1<e2,返回值小于0
当e1=e2,返回值等于0
当e1>e2 返回值大于0
提示:所以可以利用这个规律将不是int类型的返回值手动变成int型(下文float类型那里会详细说明)
使用
此处我们举三个例子,分别是int、float和结构体类型变量的比较
int类型
明确需要
我们需要三个函数:main函数(调用test函数),test函数(调用qsort函数、打印最终结果),和cmp_int函数(提供元素的比较方法)
test函数
1.创建数组
2.计算大小
3.调用qsort函数
4.打印最终结果
cmp_int函数
照着前面的固定格式,然后返回值那里就直接用
(这里我一写*,他就识别成斜体,大家直接看下面的代码吧…)
最终代码
#include<stdlib.h>int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;}void test1()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}int main()
{test1();return 0;
}
还是比较好理解的,就不做过多解释了
float类型
对于cmp_float函数的说明
问题
我们知道cmp_float函数的返回值必须是int类型的,但,
*(float*)e1 - *(float*)e2
的返回类型是float类型,在运行时会报一个警告:return”: 从“float”转换到“int”,可能丢失数据,
此处提供两种解决方法:
1.使用if else语句手动判断大小并根据情况分别返回一个负数、0、一个正数
代码如下:
if (*(float*)e1 > *(float*)e2)
{return 1;
}
else if (*(float*)e1 == *(float*)e2)
{return 0;
}
else
{return -1;
}
2.使用强制类型转换,转换成int类型
最终代码
#include<stdlib.h>int cmp_float(const void* e1, const void* e2)
{return *(float*)e1 - *(float*)e2;
}void test2()
{float f[] = { 9.0, 8.0, 7.0, 6.0 ,5.0 ,4.0 ,3.0, 2.0, 1.0 };int sz = sizeof(f) / sizeof(f[0]);qsort(f, sz, sizeof(f[0]), cmp_float);int i = 0;for (i = 0; i < sz; i++){printf("%.3f ", f[i]);}
}int main()
{test2();return 0;
}
结构体类型
如果我们想要排序结构体类型的变量,那就很有意思了,我们一步一步来分析
明确需要
main函数、test3函数、cmp_stu函数
下面我们重点解释一下test3函数和cmp_stu函数
test3函数
1.创建结构体类型的数组,并初始化
2.求数组元素个数
3.调用qsort函数,里面包含了cmp_stu函数的地址,即调用cmp_stu函数
cmp_stu函数
照猫画虎
我们按照前面的两个例子写出来的应该是
int cmp_struct(const void* e1, const void* e2)
{return *(struct*)e1 - *(struct*)e2;}
但这么写是错误的, 因为结构体是复杂对象,无法直接用 > 或 < 进行比较,那么我们就需要确定是用哪个成员去作为比较的标准
再通过->来访问相应的成员
下面给出两个例子,此处分别以年龄age作为排序标准和以名字name来排序,
cmp_stu_by_age函数
将e1和e2从void* 类型转换成结构体类型指针,然后再通过->访问相应的成员
然后直接让二者相减即可,此处在强制类型转换时别忘了写上struct就行
int cmp_stu_by_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;}
这个函数的实现还是与cmp_int函数有一些相似的,
cmp_stu_by_name函数
提示:此处比较的是字符串,同样不能用 > < 来进行比较
而是要用strcmp函数进行比较,包含头文件<string.h>
int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);}
最终代码
#include<stdlib.h>struct Stu
{char name[40];int age;
};int cmp_stu_by_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;}int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);提示,此处比较的是字符串,同样不能用 > < 来进行比较而是要用strcmp函数进行比较
}void test3()
{struct Stu s[3] = { {"zhang", 20},{"li", 30},{"wang", 40} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}int main()
{test3();return 0;
}
三个例子到这里就介绍结束了,其实这样看下来也不是很难理解。
下面我们来学习qsort函数的模拟实现,也就是优化bubble_sort函数,使它能排序任意类型的元素
模拟实现
引入:
此处提出一个问题:如果说,我不想使用qsort函数,我就想使用冒泡函数,那我要如何改进它,才能达到和qsort函数相同的效果呢?
函数调用方面的改进
接收地址
如果说想让冒泡排序函数具有排序任意类型元素的功能,那么首先,它就应该能接收任意类型元素的地址(类似于qsort中的base参数)
元素个数
函数需要知道要排序多少个元素,所以就需要传入数组的大小(类似num参数)
元素大小(宽度)
知道了待排序数组的起始位置和元素个数后,我们需要对数组中的元素进行移动操作, 那么我们就需要知道元素的大小是什么
简易版框架如下:
void bubble_sort(void* base, int sz, int width)
{int i = 0;//次数for (i = 0; i < sz; i++){//每次需要比较的元素对数int j = 0;for (j = 0; j < sz - 1 - i; j++)//比较两个元素{}}
}
疑问1:
基本框架搭建好了,那我们要如何比较两个元素呢,我们又不知道他们的类型?
所以我们在传参的时候,还需要将两个元素的比较方法(函数)一并传进bubble_sort函数中,也就是第四个参数
首先,要传入的肯定是函数的地址,
其次,我们需要返回一个值来告诉我们比较的结果是什么(此处类似qsort函数的返回值)
最后,对于要比较的两个元素,因为要求函数具有通用性,所以参数类型就是void *类型
代码如下:
void bubble_sort(void* base, int sz, int width, int(*cmp)(void* e1,void* e2) )
{int i = 0;//次数for (i = 0; i < sz; i++){//每次需要比较的元素对数int j = 0;for (j = 0; j < sz - 1 - i; j++)//比较两个元素{if(cmp()>0)//交换if(cmp()>0)//交换{}}}}
}
疑问2:if语句
if(cmp()>0)//交换{}
我们知道这个语句是比较两个元素,那我们怎么找到这俩个元素呢?
我们知道,base就是首元素的地址,
想法1
那么有人想通过加减整数来找到后面的元素,这个问题在我前面的文章提到过:因为元素是void*类型的,不知道元素大小,无法与整数进行运算
想法2
那么又有人想:将base传换成(int*)类型再运算不就行了吗,
还是不对,因为我们不知道传进来的参数究竟是什么类型,所以我们不能假定他的类型
想法3
小明这时候提出来:我们已经知道了每个元素的大小:width,那可不可以先把base转换成char*类型,再加上每个元素的字节大小width呢?
这么做就可以了,
因为char*大小是一个字节,每次移动width个字节,就进入到下一个元素中了
if语句代码如下:
if(cmp((char*)base+ j*width, (char*)base +(j+1)*width)>0)//交换
{}
疑问3:怎么交换
这里先创建一个swap函数用于两个元素的交换
void Swap(char*buf1, char*buf2)
{}void bubble_sort(void* base, int sz, int width)
{int i = 0;//次数for (i = 0; i < sz; i++){//每次需要比较的元素对数int j = 0;for (j = 0; j < sz - 1 - i; j++)//比较两个元素{if(cmp((char*)base+ j*width, (char*)base +(j+1)*width)>0)//交换{Swap((char*)base + j * width, (char*)base + (j + 1) * width);}}}
}
但是,我们仔细看,Swap函数接收的参数类型是char*,一个字节大小,如果我这个元素是8个字节类型,要怎么交换呢,
所以如果按照一个字节一个字节这么交换的方式,我们就需要知道要交换的元素的字节大小(width),以及要交换几次
代码如下:
void Swap(char*buf1, char*buf2, int width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}
简单实现(int类型)
完整代码如下:
void Swap(char*buf1, char*buf2, int width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}void bubble_sort(void* base, int sz, int width, int (*cmp)(void*e1, void*e2))
{int i = 0;//次数for (i = 0; i < sz; i++){//每次需要比较的元素对数int j = 0;for (j = 0; j < sz - 1 - i; j++)//比较两个元素{if(cmp((char*)base+ j*width, (char*)base +(j+1)*width)>0)//交换{Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);}}}
}int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;
}void test4()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
}int main()
{test4();return 0;
}
想要进行其他类型的比较只需要在调用bubble_sort函数时,将第四个参数修改成对应的比较方法即可(当然,这需要自己构建)
小提示:
->的优先级高于强制类型转换,所以要用()先将强制类型转换括起来,先转换,再访问
题外话
因为想要使bubble_sort函数具有通用性,所以我们需要将不同类型元素的比较方法的函数的地址传进来(也就是第四个参数),而这种将函数地址传进另一个函数,由这个函数去实现调用的方法,就称为回调函数(大概就是这个意思),我这几天在整理指针的知识,有时间就写一篇博客。
结语
没想到感觉没怎么写,就写了六千多字(捂脸)
只能再一次感叹C的丰富
文章到这里就结束了,希望这篇文章对你有所帮助,我们下篇文章见~
相关文章:

C语言库函数之 qsort 讲解、使用及模拟实现
引入 我们在学习排序的时候,第一个接触到的应该都是冒泡排序,我们先来复习一下冒泡排序的代码,来作为一个铺垫和引入。 代码如下: #include<stdio.h>void bubble_sort(int *arr, int sz) {int i 0;for (i 0; i < sz…...

Maven之mirrorof范围
mirrorOf 是 central 还是 * 的问题 在配置阿里对官方中央仓库的镜像服务器时,我们使用到了 <mirror> 元素。 <mirror><id>aliyunmaven</id><mirrorOf>central</mirrorOf><name>阿里云公共仓库</name><url>…...

游戏中的UI适配
引用参考:感谢GPT UI适配原理以及常用方案 游戏UI适配是确保游戏界面在不同设备上以不同的分辨率、屏幕比例和方向下正常显示的关键任务。下面是一些常见的游戏UI适配方案: 1.分辨率无关像素(Resolution-Independent Pixels)&a…...

【Linux命令详解 | gzip命令】 gzip命令用于压缩文件,可以显著减小文件大小
文章标题 简介一,参数列表二,使用介绍1. 基本压缩和解压2. 压缩目录3. 查看压缩文件内容4. 测试压缩文件的完整性5. 强制压缩6. 压缩级别7. 与其他命令结合使用8. 压缩多个文件9. 自动删除原文件 总结 简介 在Linux中,gzip命令是一款强大的文…...

IP 协议的相关特性和数据链路层相关知识总结
目录 IP 协议的相关特性 一、IP协议的特性 二、 IP协议数据报格式 三、 IP协议的主要功能 1. 地址管理 动态分配 IP地址 NAT机制 NAT背景下的通信 IPV6 2. 路由控制 3.IP报文的分片与重组 数据链路层相关知识 1、以太网协议(Ethernet) 2.M…...

探索C语言中的常见排序算法
探索C语言中的常见排序算法 排序算法是计算机科学中至关重要的基础知识之一,它们能够帮助我们对数据进行有序排列,从而更高效地进行搜索、插入和删除操作。在本篇博客中,我们将深入探讨C语言中的一些常见排序算法,包括它们的工作…...

【UE】Web Browser内嵌网页在场景中的褪色问题
使用WebBrowser放置在场景中时,网页颜色会出现异常的褪色。 这是因为 Web 浏览器插件以 sRGB 格式输出其颜色数据,而 Widget/3D Widget 需要线性 RGB 格式的数据。 可以通过创建在 3D Widget 中使用的新材质(而不是默认的 Widget3DPassthr…...

rust入门系列之Rust介绍及开发环境搭建
Rust教程 Rust基本介绍 网站: https://www.rust-lang.org/ rust是什么 开发rust语言的初衷是: 在软件发展速度跟不上硬件发展速度,无法在语言层面充分的利用硬件多核cpu不断提升的性能和 在系统界别软件开发上,C出生比较早,内…...

embed mongodb 集成spring
在property文件下添加 de.flapdoodle.mongodb.embedded.version5.0.5 spring.mongodb.embedded.storage.oplog-size0不指定数据库,会使用test, port默认是0,随机端口号。 oplog-size mac默认是192mb, 其他系统会使用5%的磁盘可用空间&#x…...

ssh远程连接服务器
一、远程连接服务器简介 二、连接加密技术简介 三、ssh服务配置 四、用户登录ssh服务 Enforcing会强制限制,如端口为22,可以访问,如果是2000端口,不能使用 Permissive是宽容的模式,不限制使用端口 Enforcing会重启失败…...
性能分析之MySQL慢查询日志分析(慢查询日志)
一、背景 MySQL的慢查询日志是MySQL提供的一种日志记录,他用来记录在MySQL中响应的时间超过阈值的语句,具体指运行时间超过long_query_time(默认是10秒)值的SQL,会被记录到慢查询日志中。 慢查询日志一般用于性能分析时开启,收集慢SQL然后通过explain进行全面分析,一…...

每日一练 | mongo集群如何创建分片键
文章目录 MongoDB是什么什么是分片键环境如何设置分片键 MongoDB是什么 MongoDB 是一个基于分布式文件存储的数据库 什么是分片键 分片:每个分片包含分片数据的一部分。每个分片可以部署为副本集。 而分片键的作用就是把数据按一定的条件分布到各个分片中&#…...

Postman
Postman 简介下载安装 简介 Postman 是一款用于测试和开发 API(应用程序编程接口)的工具,它提供了用户友好的界面和丰富的功能,帮助开发者轻松地创建、测试、调试和文档化各种类型的 API。无论是在构建 Web 应用、移动应用还是其…...

chapter 3 Free electrons in solid - 3.1 自由电子模型
3.1 自由电子模型 Free electron model 研究晶体中的电子: 自由电子理论:不考虑离子实能带理论:考虑离子实(周期性势场)的作用 3.1.1 德鲁德模型 Drude Model - Classical Free Electron Model (1)德鲁德模型 德鲁…...

搭建博客时前端美化内容CSS推荐
一、背景 在搭建博客的时候,发现对其markdown文章内容进行渲染的时候,样式调整比较花费时间 二、解决思路 自己适配样式 缺点:ROI不高 使用开源的markdown的样式:github-markdown-css 三、实现教程 1、NPM安装 npm install …...

Linux中 socket编程中多进程/多线程TCP并发服务器模型
一、循环服务器(while)【不常用】 一次只能处理一个客户端的请求,等这个客户端退出后,才能处理下一个客户端。缺点:循环服务器所处理的客户端不能有耗时操作。 模型 sfd socket(); bind(); listen(); while(1) {newfd accept();while(1){r…...

【内网穿透】如何实现在外web浏览器远程访问jupyter notebook服务器
文章目录 前言1. Python环境安装2. Jupyter 安装3. 启动Jupyter Notebook4. 远程访问4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5. 固定公网地址 前言 Jupyter Notebook,它是一个交互式的数据科学和计算环境,支持多种编程语言,如…...

win10下如何安装ffmpeg
安装ffmpeg之前先安装win10 绿色软件管理软件:scoop. Scoop的基本介绍 Scoop是一款适用于Windows平台的命令行软件(包)管理工具,这里是Github介绍页。简单来说,就是可以通过命令行工具(PowerShell、CMD等…...

分代收集 + 垃圾回收算法
分代假说 1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的 2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡 3. 跨代引用假说&…...

第三届“赣政杯”网络安全大赛 | 赛宁筑牢安全应急防线
为持续强化江西省党政机关网络安全风险防范意识,提高信息化岗位从业人员基础技能,提升应对网络安全风险处置能力。由江西省委网信办、江西省发展改革委主办,江西省大数据中心、国家计算机网络与信息安全管理中心江西分中心承办࿰…...

CHATGPT源码简介与使用指南
CHATGPT源码的基本介绍 CHATGPT源码备受关注,它是一款基于人工智能的聊天机器人,旨在帮助开发者快速搭建自己的聊天机器人,无需编写代码。下面是对CHATGPT搭建源码的详细介绍。 CHATGPT源码的构建和功能 CHATGPT源码是基于Google的自然语言…...

【C++精华铺】8.C++模板初阶
目录 1. 泛型编程 2. 函数模板 2.1 函数模板的概念及格式 2.2 函数模板的原理 2.3 模板的实例化 2.4 模板参数的匹配原则 3. 类模板 3.1 类模板格式 3.2 类模板的实例化 1. 泛型编程 什么是泛型编程?泛型编程是避免使用某种具体类型而去使用某种通用类型来进行…...

离谱的Bug
离谱的 Bug Bug 情况发现 Bug修改 Bug其他感受历史 Bug火星Spirit号Mars Global Surveyor任务 Bug 情况 有一次,我在开发一个网页应用程序时,遇到了一个令人目瞪口呆的Bug。这个Bug出现在一个特定的页面上,当用户点击某个按钮时,…...

leetcode 322. 零钱兑换
本题属于完全背包问题,但要求最少的硬币个数。于是设定dp数组的含义dp[i]:总金额为i时,能凑成i的最少硬币个数。 需要注意初始化dp数组时,除0以外的其他地方需要初始化为INT_MAX以保证在递推过程中能被正确的覆盖。 代码如下: …...

(二)结构型模式:6、外观模式(Facade Pattern)(C++实例)
目录 1、外观模式(Facade Pattern)含义 2、外观模式的UML图学习 3、外观模式的应用场景 4、外观模式的优缺点 5、C实现外观模式的简单实例 1、外观模式(Facade Pattern)含义 外观模式(Facade Pattern)…...

docker的资源控制管理——Cgroups
目录 一、对CPU使用率的控制 1.1 CPU 资源控制 1.2 cgroups有四大功能 1.3 设置cpu使用率上限 查看周期限制和cpu配额限制 进行cpu压力测试然后修改每个周期的使用cpu的时间,查看cpu使用率 1.4 设置cpu资源占用比(设置多个容器时才有效…...

less学习语法
1.CSS函数的补充 1.rgb/rgba/translate/rotate/scale 2.非常好用的css函数: var:使用css定义的变量calc:计算css值,通常用于计算元素的大小或位置blur:毛玻璃(高斯模糊)效果gradient:颜色渐变函数 var:定义变量 css中可以自定…...

在 SHELL 脚本中调用另一个 SHELL 脚本(报错: go: not found)
文章目录 在 SHELL 脚本中调用另一个 SHELL 脚本(报错: go: not found)在 SHELL 脚本中调用另一个 SHELL 脚本一个脚本sudo调另外一个脚本,报错(报错: go: not found) 在 SHELL 脚本中调用另一个…...

07微服务的事务管理机制
一句话导读 在单体应用程序中,事务通常是在单个数据库或单个操作系统中管理的,而在微服务架构中,事务需要跨越多个服务和数据库,这就使得事务管理变得更加复杂和困难。 目录 一句话导读 一、微服务事务管理的定义和意义 二、微…...

CS5523规格书|MIPI转EDP方案设计|替代LT8911芯片电路原理|ASL集睿致远CS替代龙讯
ASL芯片(集睿致远) CS5523是一款MIPI DSI输入,DP/e DP输出转换芯片,可pin to pin替代LT8911龙讯芯片。 MIPI DSI 最多支持 4 个通道,每个通道的最大运行速度为 1.5Gps。对于DP 1.2输出,它支持1.62Gbps和2.…...