C语言库函数之 qsort 讲解、使用及模拟实现

news2024/11/26 16:47:46

引入

我们在学习排序的时候,第一个接触到的应该都是冒泡排序,我们先来复习一下冒泡排序的代码,来作为一个铺垫和引入。

代码如下:

#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的丰富

文章到这里就结束了,希望这篇文章对你有所帮助,我们下篇文章见~

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

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

相关文章

基于chatgpt动手实现一个ai_translator

动手实现一个ai翻译 前言 最近在极客时间学习《AI 大模型应用开发实战营》&#xff0c;自己一边跟着学一边开发了一个进阶版本的 OpenAI-Translator&#xff0c;在这里简单记录下开发过程和心得体会&#xff0c;供有兴趣的同学参考&#xff1b; ai翻译程序 版本迭代 在学习…

C语言必会题目(2)

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; 今天继续分享C语言必会的题目&#xff0c;上一篇文章主要是一些选择题&#xff0c;而今天我们主要内容为编程题的推荐与讲解 准备好迎接下面的题了吗&#xff1f;开始发车了&#xff01;&#xff01;&#xff01; 输入…

pytest运行时参数说明,pytest详解,pytest.ini详解

一、Pytest简介 1.pytest是一个非常成熟的全功能的Python测试框架&#xff0c;主要有一下几个特点&#xff1a; 简单灵活&#xff0c;容易上手&#xff0c;支持参数化 2.能够支持简单的单元测试和复杂的功能测试&#xff0c;还可以用来做selenium、appium等自动化测试&#xf…

zookeeper案例

目录 案例一&#xff1a;服务器动态上下线 服务端&#xff1a; &#xff08;1&#xff09;先获取zookeeper连接 &#xff08;2&#xff09;注册服务器到zookeeper集群&#xff1a; &#xff08;3&#xff09;业务逻辑&#xff08;睡眠&#xff09;&#xff1a; 服务端代码…

提高生产力 | Apifox 数据结构验证最佳实践

目录 实践场景 定义返回响应 场景数据准备 校验响应数据 总结 在设计接口的过程中&#xff0c;响应数据需要和返回响应规范一一对应。这样能够确保接口的一致性和可靠性&#xff0c;并且方便接口的使用和维护&#xff0c;即使在后续迭代过程中出现问题&#xff0c;开发人员…

zabbix监控安装部署

目录 一、环境 二、配置 1.配置yum源&#xff0c;这里用的清华的 2.过滤一下安装包&#xff0c;查看依赖包 安装依赖包 3.配置数据库 开机自启 创建数据库 创建用户 授权 导入数据到数据库 查看zabbix数据库有没有表和数据 4.修改zabbix配置文件 1.修改zabbix配置…

【Java】常见面试题:多线程

文章目录 1. 谈谈进程和线程之间的区别【高频】2. java中有哪些方式来创建线程&#xff1f;3. run和start的区别【经典面试题】4. Java线程的状态5. 【线程不安全的原因】6. 就以count为例&#xff1a;一个线程加锁、一个线程不加锁&#xff0c;此时能否保证线程的安全呢&#…

client-go实战之十二:选主(leader-election)

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 本文是《client-go实战》系列的第十二篇&#xff0c;又有一个精彩的知识点在本章呈现&#xff1a;选主(leader-election)在解释什么是选主之前&…

Keepalived源码安装

文章目录 Keepalived源码安装安装准备缺少OpenSSL解决方法 Keepalived 源码安装 安装准备 tar zxf keepalived-2.2.8.tar.gz /root/ ll drwxrwxr-x. 10 1000 1000 4096 Aug 9 18:29 keepalived-2.2.8 #进入目录执行以下命令查看帮助 ./configure --help #重要编译参数 -…

QT学习笔记-oracle oci数据库驱动交叉编译并移植到ARM开发板

QT学习笔记-oracle oci数据库驱动交叉编译并移植到RK3568ARM开发板 0、背景1、搭建交叉编译环境2、交叉编译过程3、把数据库驱动部署到目标系统中 0、背景 在上一文《QT学习笔记-QT安装oracle oci驱动》中介绍了在Windows环境下使用QT访问oracle数据库时遇到驱动无法加载问题的…

kingbase:数据库启动状态

1 启停KingbaseES数据库 Linux下通过系统服务&#xff1a; root用户执行&#xff1a; service kingbase8d stop/start/restart ——注册服务的情况下 Linux下通过安装用户&#xff1a; 安装用户执行&#xff1a; sys_ctl stop/start/restart -D data路径 2 查看数据库当…

UI自动化环境的搭建(python+pycharm+selenium+chrome)

最近在做一些UI自动化的项目&#xff0c;为此从环境搭建来从0到1&#xff0c;希望能够帮助到你&#xff0c;同时也是自我的梳理。将按照如下进行开展&#xff1a; 1、python的下载、安装&#xff0c;python环境变量的配置。 2、pycharm开发工具的下载安装。 3、selenium的安装。…

【Java】一只小菜坤的编程题之旅【3】

文章目录 1丶判定是否互为字符重排2、杨辉三角3丶某公司的1个面试题&#xff08;字符串包含问题&#xff09; 1丶判定是否互为字符重排 这个题我们用一个非常简单的思想就能实现&#xff0c;我们先将字符串转换为字符数组&#xff0c;然后对字符数组进行排序&#xff0c;然后再…

Codeforces Round 893 (Div. 2)ABC

Codeforces Round 892 (Div. 2) 目录 A. United We Stand题目大意思路代码 B. Olya and Game with Arrays题目大意思路代码 C. Another Permutation Problem题目大意思路代码 A. United We Stand 题目大意 给你一个数组&#xff0c;把这个数组分成两个数组a和b&#xff0c;使…

03_013内存分配api以及页表详解

前言 之前文章中物理ram中的最小单位一直用页来表示 这次又描述的详细了点 物理ram的最小单位 有的地方叫 块,框,页帧 在虚拟空间中最小单位也叫页 需要好好区分 不过后来想想管你虚拟页还是物理ram页 都存在物理ram上 都能想成一 一对应的关系 所以大家都叫页好像也行 内存分…

【Unity3D】Shader Graph节点

1 前言 Shader Graph 16.0.3 中有 208 个 Node&#xff08;节点&#xff09;&#xff0c;本文梳理了 Shader Graph 中大部分 Node 的释义&#xff0c;官方介绍详见→Node-Library。 Shader Graph 通过图像的形式表达了顶点变换和片元着色流程&#xff0c;其背后都是一些列的数学…

保持城市天际线(力扣)贪心 JAVA

给你一座由 n x n 个街区组成的城市&#xff0c;每个街区都包含一座立方体建筑。给你一个下标从 0 开始的 n x n 整数矩阵 grid &#xff0c;其中 grid[r][c] 表示坐落于 r 行 c 列的建筑物的 高度 。 城市的 天际线 是从远处观察城市时&#xff0c;所有建筑物形成的外部轮廓。…

【系统架构】分布式系统架构设计

1 分布式系统是什么 分布式系统是指由多个计算机节点组成的一个系统&#xff0c;这些节点通过网络互相连接&#xff0c;并协同工作完成某个任务。 与单个计算机相比&#xff0c;分布式系统具有更高的可扩展性、可靠性和性能等优势&#xff0c;因此广泛应用于大规模数据处理、高…

大数据-玩转数据-Flink 自定义Sink(Mysql)

一、说明 如果Flink没有提供给我们可以直接使用的连接器&#xff0c;那我们如果想将数据存储到我们自己的存储设备中&#xff0c;mysql 的安装使用请参考 mysql-玩转数据-centos7下mysql的安装 创建表 CREATE TABLE sensor (id int(10) ) ENGINEInnoDB DEFAULT CHARSETutf8二…

使用日志来监控应用

根据提取规则运行的位置可以分为两类做法&#xff0c;一个是在中心端&#xff0c;一个是在日志端。 中心端就是把要处理的所有机器的日志都统一传到中心&#xff0c;比如通过 Kafka 传输&#xff0c;最终落到 Elasticsearch&#xff0c;指标提取规则可以作为流计算任务插到 Ka…