排序大师:探秘C语言中神奇的qsort库函数

news2025/1/15 23:28:49

本篇文章中会详细讲解C语言中的qsort库函数。我准备分2个方面来讲:

  1. qsort如何使用。
  2. 模拟实现qsort的效果。(注意:只是用冒泡排序的思想实现类似的效果,实际qsort的底层采用的是快速排序的思想。)

在这里插入图片描述

如何使用

先来看看qsort函数的描述:

在这里插入图片描述
翻译一下:qsort的使用需要包含stdlib.h头文件。qsort的形式是:

void qsort (void* base, size_t num, size_t size,
            int (*compar)(const void*,const void*));

qsort的作用是:对数组元素进行排序。

  1. 该函数使用compar函数确定顺序,对由base指向的数组的num个元素进行排序,每个元素大小为size字节。
  2. 该排序算法通过使用指向它们的指针调用指定的compar函数来比较元素对。
  3. 该函数不返回任何值,但修改由base指向的数组的内容,重新排列其元素,如compar所定义的那样。
  4. 等价元素的顺序是未定义的。

如果你第一次接触这个函数,可能会觉得云里雾里的。我们继续阅读文档:
在这里插入图片描述

对于参数的解释:

  1. base: 指向要排序的数组中第一个对象的指针,转换为void*类型。
  2. num: base所指向的数组中元素的数量。size_t是一个无符号整数类型。
  3. size: 数组中每个元素的字节大小。size_t是一个无符号整数类型。
  4. compar: 一个函数指针。

下面再详细讲解一下compar这个参数是干啥的。它是一个指向比较两个元素的函数的指针。这个函数会被qsort反复调用来比较两个元素。它应该遵循以下原型:

int compar (const void* p1, const void* p2);

以上函数接受两个指针作为参数(都转换为const void*)。该函数通过以稳定和可传递的方式返回元素的顺序来定义元素的顺序:

返回值意义
<0p1指向的元素小于p2指向的元素
0p1指向的元素等于p2指向的元素
>0p1指向的元素大于p2指向的元素

对于可以使用常规关系运算符进行比较的类型,通用的比较函数可能如下所示:

int compareMyType (const void * a, const void * b)
{
	if ( *(MyType*)a <  *(MyType*)b ) 
		return -1;
		
	if ( *(MyType*)a == *(MyType*)b ) 
		return 0;
		
	if ( *(MyType*)a >  *(MyType*)b ) 
		return 1;
}

以上是把文档中的内容翻译过来。这里我再总结一下:qsort函数是用来排序一个数组的,传的4个参数分别决定了:数组从哪开始、有几个元素、每个元素多大、如何比较。对于最后一点“如何比较”,是通过一个函数指针,以回调函数的形式实现的。这个函数指针指向一个函数,函数的参数是(const void* p1, const void* p2),返回类型是int,返回值大于、小于、等于0,分别代表着p1指向的元素大于、小于、等于p2指向的元素。

举个例子:排序一个整型数组。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int cmp_int(const void* p1, const void* p2)
{
	assert(p1 && p2);

	return *(int*)p1 - *(int*)p2;
}

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, sz, sizeof(arr[0]), cmp_int);

	for (int i = 0; i < sz; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}

输出结果:
在这里插入图片描述
大家重点关注我如何传参的,以及compar函数是如何实现的。

传参:

  1. arr是数组名,表示数组首元素的地址。
  2. sz是数组元素个数。
  3. sizeof(arr[0])表示数组每个元素多大,当然也可以写sizeof(int)。
  4. cmp_int是函数名,表示函数的地址。

cmp_int函数的实现:void*类型的指针不能直接解引用,需要强制类型转换成int*类型再解引用。解引用后就能得到2个整数,返回它们之间的差即可,因为p1指向的元素大于p2指向的元素时,返回正数;因为p1指向的元素小于p2指向的元素时,返回负数;因为p1指向的元素等于p2指向的元素时,返回0。

模拟实现

qsort底层采用的是快速排序算法。如果采用快速排序算法来模拟实现qsort的效果,有一点复杂,不适合初学者学习。这里我使用冒泡排序的算法,写一个通用的bubble_sort函数。

下面跟着我一步一步实现。先搭个架子出来:

void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void* p1, const void* p2))
{
	assert(base);
	assert(cmp);

	for (size_t i = 0; i < num - 1; ++i)
	{
		for (size_t j = 0; j < num - 1 - i; ++j)
		{

		}
	}
}

以上代码最重要的是明白2层循环的结束条件。

对于外层循环,决定了冒泡排序的“趟数”,由于一趟冒泡排序会把1个数排到正确的位置,那么num-1趟冒泡排序就会把num-1个数排到正确的位置,剩下的那个数的位置自然也就正确了。

对于内层循环,决定了一趟冒泡排序要比较几次。仔细想一下:一共有num个数,第一趟冒泡排序需要比较num-1次,第二趟冒泡排序需要比较num-2次,第三趟冒泡排序需要比较num-3次,后面依次递减。由于i从0开始一次递增,所以内层循环的判断条件就是j<num-1-i。

冒泡排序的思想是:每次比较2个数时,如果不满足顺序,就交换。此时我们要调用cmp函数,如果返回值是正数,说明前面的数大于后面的数,假设我们要排升序,就需要交换这2个数。

那传给cmp函数的参数是什么呢?本质上,需要比较的是数组中下标为j和j+1的元素。那如何找到这2个元素呢?这就要涉及到指针的知识点。

先说“正常”的情况。假设有一个数组是int arr[10];,下标为j和j+1的元素的地址分别为:arr+j和arr+j+1,这是因为arr作为数组名,表示数组首元素地址,类型是int*,所以+j后就会跳过j个int,就是下标为j的元素,j+1同理。

再回来看cmp函数的参数。是2个const void*类型的元素,其实就是要找到数组中下标为j和j+1的元素的地址。但是在bubble_sort函数内部,只知道数组起始地址base、数组元素个数num、数组单个元素大小size,所以下标为j的元素就应该是:(char*)base + j*size。解释一下:base作为数组首元素地址,强制类型转换成char*的好处是,加多少就会跳过多少个字节。由于要跳过j个元素,其实就是要跳过j*size个字节,所以应该在(char*)base的基础上加上j*size。同理,下标为j+1的元素的地址就应该是:(char*)base + (j+1)*size

综上所述,继续写代码:

void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void* p1, const void* p2))
{
	assert(base);
	assert(cmp);

	for (size_t i = 0; i < num - 1; ++i)
	{
		for (size_t j = 0; j < num - 1 - i; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				// 交换
				swap_bit((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}

那交换的逻辑是什么呢?为什么要传3个参数呢?

首先回答第2个问题。前2个参数表示要交换的元素的起始地址,但是我们不知道单个元素有多大。可能会有朋友说:把这2个地址强制类型转换成char*类型再相减,根据数组中元素是连续存放的规则,就能得到单个元素的大小了。但是这样太麻烦了,不如直接传过去。

接下来回答如何交换的问题。只需要一个一个字节交换,所以仍然需要先把指针强制类型转换成char*类型。

void swap_bit(void* p1, void* p2, size_t size)
{
	assert(p1 && p2);

	for (size_t i = 0; i < size; ++i)
	{
		char tmp = ((char*)p1)[i];
		((char*)p1)[i] = ((char*)p2)[i];
		((char*)p2)[i] = tmp;
	}
}

其实写到这,程序已经可以跑了。但是还有一点可以优化:如果某一趟冒泡排序一个数都没有交换,那么就已经有序了,不用继续排序了。

void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void* p1, const void* p2))
{
	assert(base);
	assert(cmp);

	for (size_t i = 0; i < num - 1; ++i)
	{
		int flag = 1; // 假设已经有序了
		for (size_t j = 0; j < num - 1 - i; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				flag = 0;
				// 交换
				swap_bit((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
		if (flag)
		{
			return;
		}
	}
}

2个字:完美!

以下是完整的实现代码和测试代码:

#include <stdio.h>
#include <assert.h>

void swap_bit(void* p1, void* p2, size_t size)
{
	assert(p1 && p2);

	for (size_t i = 0; i < size; ++i)
	{
		char tmp = ((char*)p1)[i];
		((char*)p1)[i] = ((char*)p2)[i];
		((char*)p2)[i] = tmp;
	}
}

void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void* p1, const void* p2))
{
	assert(base);
	assert(cmp);

	for (size_t i = 0; i < num - 1; ++i)
	{
		int flag = 1; // 假设已经有序了
		for (size_t j = 0; j < num - 1 - i; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				flag = 0;
				// 交换
				swap_bit((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
		if (flag)
		{
			return;
		}
	}
}

int cmp_int(const void* p1, const void* p2)
{
	assert(p1 && p2);

	return *(int*)p1 - *(int*)p2;
}

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

	for (int i = 0; i < sz; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}

总结

本篇博客主要讲解了qsort函数如何使用,以及如何把bubble_sort改造的更加通用。大家重点掌握qsort的使用,模拟实现只是帮助大家理解。

感谢大家的阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/491464.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

反调试与反反调试

参考文本 (190条消息) C 反反调试&#xff08;NtQueryInformationProcess&#xff09;_(-: LYSM :-)的博客-CSDN博客 Windows 平台反调试相关的技术方法总结—part 2 - 先知社区 C/C MinHook 库的使用技巧 - lyshark - 博客园 (cnblogs.com) (177条消息) C 反反调试&#x…

C结构简单而不失强大的表格

2023年了&#xff0c;想必已经不会有人对嵌入式开发中“数据结构&#xff08;Data Structure&#xff09;”的作用产生疑问了吧&#xff1f;无论你是否心存疑惑&#xff0c;本文都将给你一个完全不同的视角。 每每说起数据结构&#xff0c;很多人脑海里复现的一定是以下的内容&…

unity中用异步的whenAny,实现:当点击铲子任一部件,拾取整个铲子

一、铲子的组成 铲子包含很多部件组成&#xff0c;当拾取铲子的时候&#xff0c;只要点击铲子的任意一个部件就可以。 如图&#xff0c;点击【木柄】、【螺母】、【铁铲】都可以拾取该物体。 &#xff08;1&#xff09;打开高亮 &#xff08;2&#xff09;等待土铲被点击&…

为什么要通过API接口来获取数据

API接口&#xff08;应用编程接口 application/programming接口&#xff09;&#xff0c;准许应用程序通过定义的接口标准来访问另一个应用程序或服务的编程方式。简单来说&#xff0c;API就是两个软件或系统之间的通信语言或接口。 在当今的互联网时代&#xff0c;数据无处不…

Geospatial和Redis事务操作

一、Geospatial 1.简介 基于位置信息服务 (Location-Based Service,LBS) 的应用。 Redis3.2 版本后增加了对 GEO 类型的支持。主要来维护元素的经纬度。redis 基于这种类型&#xff0c;提供了经纬度设置、查询、范围查询、距离查询、经纬度hash等一些相关操作。 2.GEO底层结构…

DataEase 数据源插件分享 - 时序数据库 InfluxDB

前言 InfluxDB 是一个时序数据库&#xff0c;使用的是非标准的 SQL 语法&#xff0c;我使用 DataEase 的插件扩展机制开发了此数据源插件&#xff0c;在这里共享出来&#xff0c;想用的朋友可以下载安装使用。 插件包下载地址 https://north-dataease-1251506367.cos.ap-bei…

Centos 7.X WordPress博客网站详细教程 FTP/PHP/mysql/Apache环境构建

此教程适用于服务器系统为centos 7.x&#xff0c;php安装版本为7.4&#xff0c;mysql安装本部为5.7. 一、mysql安装 1.1 安装三个工具 yum install wget yum install vim yum install unzip 1.2 下载并安装msql 在线下载安装包&#xff1a; wget https://dev.mysql.com/g…

JZS-7/221静态可调延时中间继电器 JOSEF约瑟

JZS-7/2系列静态可调延时中间继电器品牌&#xff1a;JOSEF约瑟型号&#xff1a;JZS-7/2名称&#xff1a;静态可调延时中间继电器额定电压&#xff1a;48380V触点容量&#xff1a;10A/250V返回系数&#xff1a;≤15%延时范围&#xff1a;15ms3s15ms5s15ms10s JZS-7/2系列静态可…

SQL中使用的运算符号详解

文章目录 前言1. 算术运算符加法与减法运算符乘法与除法运算符求模&#xff08;求余&#xff09;运算符 2. 比较运算符1&#xff0e;等号运算符2&#xff0e;安全等于运算符3&#xff0e;不等于运算符4. 空运算符5. 非空运算符6. 最小值运算符7. 最大值运算符8. BETWEEN AND运算…

射频功率放大器(RF PA)线性化技术及分类介绍

基本概念 射频功率放大器(RF PA)是发射系统中的主要部分&#xff0c;其重要性不言而喻。在发射机的前级电路中&#xff0c;调制振荡电路所产生的射频信号功率很小&#xff0c;需要经过一系列的放大&#xff08;缓冲级、中间放大级、末级功率放大级&#xff09;获得足够的射频功…

Zabbix“专家坐诊”第190期问答汇总

问题一 Q&#xff1a;请问为啥用拓扑图监控交换机接口流量&#xff0c;获取不到数据&#xff0c;显示未知&#xff0c;键值也没错 &#xff0c;最新数据也能看到&#xff0c;是什么原因呢&#xff1f; A&#xff1a;把第一个值改成主机名。 问题二 Q&#xff1a;请问下zabbi…

如何进行AI换脸,AI换脸从 “0“ 到 “1” 详细教程 ——从配置环境开始

后续文章读起来可能会影响观看可以前往鄙人博客查看&#xff1a;http://www.anyuer.club/?id199 前言&#xff1a; 本人吃计算机这口饭的&#xff0c;说实话AI换脸很火的时候自己却没碰&#xff0c;挺吃亏的&#xff0c;最近时间比较充裕&#xff0c;整理了一下AI换脸的一个简…

Pyecharts 输出到 html 白屏?终极解决方案来了。

问题起因 公司内部网络&#xff0c;想要做个饼图输出到 html 。 找了教程&#xff1a;https://pyecharts.org/#/zh-cn/quickstart 我看教程写得这么规范&#xff0c;直接 CtrlC&#xff0c;CtrlV&#xff0c;百度来的代码怎么可能会有问题嘛&#xff01; 人生处处有惊喜。 样…

SpringBoot中策略模式+工厂模式业务实例(接口传参-枚举类查询策略映射关系-执行不同策略)规避大量if-else

场景 设计模式-策略模式在Java中的使用示例&#xff1a; 设计模式-策略模式在Java中的使用示例_java 策略模式示例_霸道流氓气质的博客-CSDN博客 上面讲了策略模式在Java中的使用示例。 下面看一个在SpringBoot中的实际使用示例。 业务场景: 有多个煤矿&#xff0c;信号灯…

推荐一些非常好用的DNS服务器

推荐一些非常好用的DNS服务器 1、114公共DNS服务器 1&#xff09; 老牌的114DNS&#xff0c;全国三网通用高速&#xff0c;纯净无劫持无需再忍受被强扭去看广告或粗俗网站之痛苦 DNS地址为&#xff1a;114.114.114.114 和 114.114.115.115 2&#xff09;拦截 钓鱼病毒木马网…

三顾茅庐,七面阿里,终拿25k*16offer,我的面试历程

写在片头&#xff1a;声明&#xff0c;勿杠 首先简单说一下&#xff0c;这三次面试阿里并不是一次性去面的&#xff0c;实际上第一次面试时候还在大四&#xff0c;找的实习岗&#xff0c;不太清楚是什么部门&#xff0c;别问我为什么还记得面试题&#xff0c;有记录和复盘的习…

DX算法还原

早在之前作者就写过一篇关于顶象的滑块验证&#xff0c;潦潦草草几句话就带过了。 出于互相学习的想法&#xff0c;给了一个大学生&#xff0c;奈何不讲武德把源码甩群里了&#xff0c;虽然在大佬们眼里不难&#xff0c; 不过拿着别人的东西乱传还是不太好。自认倒霉&#xf…

基于max30102的物联网病房监测系统(传感驱动和数据处理)

目录 一、实物展示 二、主体介绍 三、MAX30102的驱动 四、MAX30102的数据处理 奋斗一个星期&#xff0c;每个引脚都是扒皮焊接然后再把皮包回去的。这几天吸的垃圾气体感觉要少活两年。 一、实物展示 这次吸取上次教训&#xff0c;把线捆起来好多了 二、主体介绍 用的传感…

Python进阶篇

大家好&#xff0c;我是易安&#xff01;今天我们继续Python的学习&#xff0c;内容稍微有些多&#xff0c;不过我会尽可能举一些例子让你理解。 对象比较与拷贝 在前面的学习中&#xff0c;我们其实已经接触到了很多 Python对象比较和复制的例子&#xff0c;比如下面这个&…

【JVM】面试题总结

JVM 1、JVM 的运行时内存区域是怎样的2、堆和栈的区别3、Java 中的对象一定在堆上分配内存吗4、什么是 Stop The World5、JVM 如何判断对象是否存活6、JVM 有哪些垃圾回收算法7、什么是三色标记算法8、新生代和老年代的GC算法9、新生代和老年代的垃圾回收器有何区别10、Java 中…