【C语言加油站】qsort函数的模拟实现

news2025/1/18 4:51:04

qsort函数的模拟实现

  • 导言
  • 一、回调函数
  • 二、冒泡排序
    • 2.1 冒泡排序实现升序
  • 三、qsort函数
    • 3.1 qsort函数的使用
    • 3.2 比较函数
  • 四、通过冒泡排序模拟实现qsort函数
    • 4.1 任务需求
    • 4.2 函数参数
    • 4.3 函数定义与声明
    • 4.4 函数实现
      • 4.4.1 函数主体
      • 4.4.2 比较函数
      • 4.4.3 元素交换
    • 4.5 my_qsort函数测试
  • 五、知识点总结
  • 结语

封面

导言

大家好,很高兴又和大家见面了!!!
在数组篇章中,咱们有介绍过一种排序的方式——冒泡排序。不知道大家还有没有印象,如果没印象也没关系,等会我们会再简单介绍一下,今天我们要介绍的主角是C语言提供的一个进行排序的库函数——qsort。下面我们就开始今天的内容吧!!!

一、回调函数

在介绍qsort函数之前,我们需要先了解一个概念——回调函数。

所谓的回调函数就是通过函数指针调用的函数。如下所示:

//回调函数
void test1()
{
	printf("hehe\n");
}
int main()
{
	void (*p)() = test1;
	p();
	return 0;
}

在这个例子中,我们将test1这个函数的地址存放进函数指针p中,然后通过函数指针p来调用这个test1函数,此时的test1函数就是回调函数;

相信冰雪聪明的各位应该一看就会了,下面我们再来复习一下冒泡排序;

二、冒泡排序

所谓的冒泡排序,我们可以简单的理解为就是将一组数,通过相邻两个元素直接进行比较,从而达到排序的作用,如图所示:

冒泡排序

我们需要将这些气泡从小到大的顺序从上往下排列。
此时我们要完成一趟排序的话,我就需要从上往下将这些气泡两两之间进行比较:

  • 当发现上面的气泡比下面的大时,我们就需要将它们两个换位置;
  • 在经过两两之间的重复比较与换位后,我们就可以在一趟排序中奖最大的气泡放在最下面;

也就是说每完成一趟冒泡排序,我们就能确定一个气泡的位置,最终就能将所有的气泡按从小到大的顺序从上往下排列。

为了帮助大家更好的理解,下面我们就来实现一下冒泡排序;

2.1 冒泡排序实现升序

//冒泡排序
void Bubble(int* arr, int sz)
{
	//排序趟数
	for (int i = 0; i < sz - 1; i++)
	{
		//每一趟排序次数
		for (int 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;
			}
		}
	}
}

我们来看一下排序的效果:
冒泡排序2
冒泡排序的排序逻辑不难,就是两个元素相邻的元素比较,直到没有元素需要交换为止;
需要比较的总趟数比元素个数少一;
每一趟排序的次数,要比前一趟少一;

C语言为了帮助程序猿提高需要排序时的编程效率,它为我们提供了一个库函数——qsort函数;

三、qsort函数

qsort函数是C语言程序猿提供的可以直接使用的排序库函数。它是通过快速排序来实现的一个排序函数,它的执行效率要高于冒泡排序,下面我们通过 MSDN 来看一下这个库函数;
(这里我们就不展开讨论什么是快速排序了,后面有机会再给大家介绍。)

qsort函数
从qsort函数的介绍中我们可以得到以下的信息:

  1. qsort 函数是一个无返回类型的函数,接收排序对象的参数是一个无类型的指针型参数,函数参数中的比较函数的两个参数也是无类型的指针型的参数;
  2. qsort函数中的比较函数是一个返回类型为整型的函数;
  3. 我们在排序进行排序时,需要告诉函数排序对象的大小以及排序对象的元素所占空间大小;

我们继续往下看:

qsort函数2
通过这里的介绍,我们可以得到以下信息:

  1. 比较函数是用户自己提供的,函数有两个无类型指针型的参数;

  2. 函数的返回值需要按照以下标准:

    • 当参数1<参数2时,返回值<0;
    • 当参数1=参数2时,返回值=0;
    • 当参数1>参数2时,返回值>0;
  3. 通过这个比较函数的返回值,我们可以得到一个递增的数组;

  4. 当我们需要得到一个递减的数组时,需要将参数1和参数2进行换位,使其满足一下条件:

    • 当参数1<参数2时,返回值>0;
    • 当参数1=参数2时,返回值=0;
    • 当参数1>参数2时,返回值<0;

从qsort函数的参数类型我们可以得知,它可以接收所有类型的数组。我们前面展示的冒泡排序的函数,它能接收的只有我们限定好的对应类型的数组,这就是qsort函数的强大之处,那它具体是如何使用的呢?下面我们就来探讨一下;

3.1 qsort函数的使用

qsort函数本身需要四个参数:排序对象数组、数组大小、数组元素大小和比较函数。下面我们准备两个不同类型的数组,一个是int类型一个是char类型,为了更好的观察,此时我们将这两种数组排序封装成两个函数,这样我们只需要在主函数内调用这两个函数就可以了,如下所示:

//qsort排序整型数组
void test2()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	//通过sizeof计算数组大小
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前数组元素顺序>:");
	print(arr, sz);
	//通过qsort实现升序排列
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	printf("排序后数组元素顺序>:");
	print(arr, sz);

}
//qsort排序字符型数组
void test3()
{
	char ch[] = { '9','8','7','6','5','4','3','2','1','0' };
	//通过sizeof计算数组大小
	int sz = sizeof(ch) / sizeof(ch[0]);
	printf("排序前数组元素顺序>:");
	print(ch, sz);
	//通过qsort实现升序排列
	qsort(ch, sz, sizeof(ch[0]), cmp_char);
	printf("排序后数组元素顺序>:");
	print(ch, sz);

}

现在我们要思考一下,对于这两个数组,我们应该如何进行元素间的比较;

3.2 比较函数

我们再来看一下这个比较函数的介绍:

int(__cdecl* compare)(const void* elem1, const void* elem2);
//int——函数返回类型为整型
//__cdecl——函数调用方法:所有参数从右到左依次入栈
//const void*——参数类型为不可修改的无类型指针

这里我们简答介绍一下__cdecl

__cdeclC Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。

这个我们简单了解一下就行,不需要去深究,我们现在的重点是看其它的部分,如果我们将__cdecl省略的话,我们就能得到int(*compare)(const void* elem1, const void* elem2)

这个代码有没有一种熟悉的感觉?如果我们将这个代码格式化书写的话,它是不是就应该表示为:

type(*point_name)(parameter_type,parameter_type);
//type——函数返回类型;
//*——指针标志
//point_name——指针变量名
//parameter_type——参数类型

这个格式正是函数指针的创建格式,也就是说,这里的compare是一个函数指针,而且这个函数的参数类型还是不可修改的无类型指针。

下面我们根据这里的比较函数的格式来定义一下整型数组的比较函数与字符数组的比较函数:

//比较函数——整型数组
int cmp_int(const void* p1, const void* p2);
//比较函数——字符数组
int cmp_char(const void* p1, const void* p2);

根据前面的介绍,我们只需要让这个比较函数的两个参数进行比较后返回对应的整型值就可以了。

  • 对于整型来说,我们不难想象两个整数要比较大小后返回一个整型值,我们可以通过作差来实现,但是,此时的参数为void*类型,我们不能对这个类型的指针进行解引用,那该怎么办呢?
    • 强制类型转换——我们可以先将这个指针进行强制类型转换成int*,然后再对指针进行解引用,最后完成两个整型值作差,并将结果返回给函数就可以了。因此,我们可以编写代码:
//比较函数——整型数组
int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
  • 同理,对于字符来说,它们要比较大小的话是根据对应的ASCII码值来进行比较,所以同样也是整型类型的比较,因此,我们也是可以通过将两个字符进行作差,并返回差值给比较函数:
//比较函数——字符数组
int cmp_char(const void* p1, const void* p2)
{
	return *(char*)p1 - *(char*)p2;
}

下面我们就来测试一下:

qsort函数3
可以看到,此时我们通过qsort函数实现了对字符数组和整型数组的排序。下面我们需要思考一下,我们可不可以通过冒泡排序来实现qsort函数呢?下面我们来一步一步的进行探讨;

四、通过冒泡排序模拟实现qsort函数

4.1 任务需求

我们现在需要使用冒泡排序的方式来编写一个可以对任一类型的数组进行排序的my_qsort函数;

4.2 函数参数

既然是模拟的qsort函数,所以我们可以按照qsort函数的参数来进行传参,如下所示:

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

我们依然传入四个参数——排序对象,数组大小,元素所占空间大小以及一个排序函数指针;

4.3 函数定义与声明

为了简化编写,这里我们直接将函数定义在test4这个函数的上方,参数的类型也是仿造qsort进行定义,如下所示:

//模拟实现qsort函数
void my_qsort(void* base, int sz, int w, int(*cmp_int)(const void*, const void*))
{

}

因为我们此时测试的是整型数组,所以对于这个比较函数的编写,我们也是根据qsort函数的比较函数的要求进行编写,如下所示:

//比较函数
int cmp_int(const void* p1, const void* p2)
{
	//升序排列
	return *(int*)p1 - *(int*)p2;
	//降序排列
	return *(int*)p2 - *(int*)p1;
}

在完成了这两步操作后,接下来我们就需要开始对my_qsort这个函数进行实现了;

4.4 函数实现

4.4.1 函数主体

既然我们这里是通过冒泡排序实现的qsort,那冒泡排序的主体就不能掉,如下所示:

//模拟实现qsort函数
void my_qsort(void* base, int sz, int w, int(*cmp_int)(const void*, const void*))
{
	//排序总次数
	for (int i = 0; i < sz - 1; i++)
	{
		//一次排序所需次数
		for (int j = 0; j < sz - 1 - i; j++)
		{

		}
	}
}

我们现在要思考一下,我们应该如何对不同类型的对象的元素进行判断,从而决定是否进行元素之间的交换?其实这里qsort已经在参数中给了我们答案——比较函数。

4.4.2 比较函数

当要进行升序排列时,比较函数的返回值有三种情况:

  1. 当参数1<参数2时,返回值<0;
  2. 当参数1=参数2时,返回值=0;
  3. 当参数1>参数2时,返回值>0;

当要进行降序排列时,比较函数的返回值有三种情况:

  1. 当参数1<参数2时,返回值>0;
  2. 当参数1=参数2时,返回值=0;
  3. 当参数1>参数2时,返回值<0;

实际上不管是要进行升序排列还是降序排列,当返回值大于0时,我们才需要对数组的元素进行交换,因此我们可以将比较函数的返回值作为判断依据,如下所示:

//模拟实现qsort函数
void my_qsort(void* base, int sz, int w, int(*cmp_int)(const void*, const void*))
{
	//排序总次数
	for (int i = 0; i < sz - 1; i++)
	{
		//一次排序所需次数
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (cmp_int((base + j), (base + (j + 1))) > 0)
			{

			}
		}
	}
}

那是不是这样就完了呢?并不是,如果像这样编写,是不对的,现在我们需要注意一个点:

  • base是void*类型的指针,我们不能对这个类型的指针进行解引用以及加减整数等操作;

所以我们在进行加减整数时要先将它进行强制类型转换,但是我们要转换成什么类型呢?

我们知道,对于不同类型的元素所占内存空间大小是不相同的,但是,它们都有一个共同点:

  • 不同类型元素所占空间大小为字符类型的元素所占空间大小的整数倍;

对于不同类型的指针来说,它们在进行加减整数时,它们也有一个共同点:

  • 指针变化的大小为对应类型所占空间大小的整数倍:

根据这两点,我们来设想一下,如果我们将其转换成char*类型的指针,那我们在进行加减整数时,指针变化的大小就是对应的整数,因为,char所占内存空间大小为1个字节。

那也就是说,如果我要用char*的指针,完成整型的加减整数,因为int所占空间大小为char的四倍,是不是就相当于我需要加减整数的4倍。因此,我们就可以将代码修改一下:

//模拟实现qsort函数
void my_qsort(void* base, int sz, int w, int(*cmp_int)(const void*, const void*))
{
	//排序总次数
	for (int i = 0; i < sz - 1; i++)
	{
		//一次排序所需次数
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (cmp_int(((char*)base + j * w), ((char*)base + (j + 1) * w)) > 0)
			{

			}
		}
	}
}

现在简单的部分我们已经完成了,剩下的就是最难的部分——实现任一类型数组的元素之间的交换。

4.4.3 元素交换

对于char*的指针来说,它一次解引用访问的空间大小只有一个字节,根据前面的介绍:
如果我要访问一个整型元素,那我是不是只需要通过char*的指针访问4次就可以了;
如果有一个占据7个字节空间大小的元素,那我是不是也只需要通过char*的指针访问7次就可以了。
所以我们要进行元素交换的话,也是可以通过char*的指针来实现的。代码如下:

//模拟实现qsort函数
void my_qsort(void* base, int sz, int w, int(*cmp_int)(const void*, const void*))
{
	//排序总次数
	for (int i = 0; i < sz - 1; i++)
	{
		//一次排序所需次数
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (cmp_int(((char*)base + j * w), ((char*)base + (j + 1) * w)) > 0)
			{
				//将元素地址存入char*的指针中
				char* p1 = (char*)base + j * w;
				char* p2 = (char*)base + (j + 1) * w;
				//通过char*指针访问元素
				for (int k = 0; k < w; k++)
				{
					char tmp = *p1;
					*p1 = *p2;
					*p2 = tmp;
					p1++;
					p2++;
				}
			}
		}
	}
}

现在咱们的my_qsort函数已经编写完了,它现在到底能不能实现交换不同类型的数组呢?下面我们就来测试一下;

4.5 my_qsort函数测试

为了有更明显的效果,这里我们测试三个类型的数组——整型数组、字符数组和结构体数组;

my_qsort函数
可以看到,此时我们成功的对这三种类型的数组进行了排序。

五、知识点总结

今天介绍的内容是一个综合性很强的内容,我们在模拟实现的过程中,有用到指针中的以下知识点:

  • 指针类型的意义
  • 一维数组传参
  • void*类型的指针
  • 函数指针
  • 回调函数——比较函数的函数指针调用的比较函数就是回调函数
  • 指针±整数

在编写的过程中可能没有什么感觉,但是现在回顾一下才会发现,原来要模拟qsort函数,仅仅指针这个篇章的内容就需要这么多的知识储备,所以还是得好好学习,提升自己的知识储备才行啊。

结语

到这里,咱们今天的内容就全部介绍完了,今天我们详细介绍了qsort函数以及使用冒泡排序模拟实现qsort函数,最后对这个篇章的知识点做了一个总结。

希望这个篇章的内容,能帮助大家更好的理解指针的相关知识点及其使用。感谢大家的翻阅,咱们下一篇再见!!!

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

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

相关文章

Linux下I2C调试工具--for--Zynq MPSOC/Jetson Xavier

Linux下I2C调试工具 1、简介 i2c-tools是一个专门调试i2c的工具&#xff0c;无需编写任何代码即可轻松调试IC设备&#xff0c;可获取挂载的设备及设备地址&#xff0c;还可以在对应的设备指定寄存器设置值或者获取值等功能。i2c-tools有如下几个常用测试命令i2cdetect, i2cdu…

Stable LM Zephyr 3B:手机上的强大LLM助手

概览 最近&#xff0c;Stability.ai宣布开源了Stable LM Zephyr 3B&#xff0c;这是一个30亿参数的大语言模型&#xff08;LLM&#xff09;&#xff0c;专为手机、笔记本等移动设备设计。其突出的特点是参数较小、性能强大且算力消耗低&#xff0c;能够自动生成文本、总结摘要…

arcgis更改服务注册数据库账号及密码

最近服务器数据库密码换了&#xff0c;gis服务也得换下数据库连接密码。传统官方的更改方式&#xff08;上传连接配置文件&#xff09;&#xff1a; ArcGIS Server数据库注册篇(I) — 更新数据库密码_arcgis server sde换密码-CSDN博客 方式太麻烦了&#xff0c;需要安装ArcG…

Tektronix泰克TCP303示波器电流探头

主要特点和优点&#xff1a; ● 交流/直流测量功能 ● DC~100MHz电流探头放大器&#xff08;TCPA300&#xff09;&#xff0c;当使用&#xff1a; - DC~100MHz, 30A DC&#xff08;TCP312&#xff09; - DC~50MHz, 50A DC&#xff08;TCP305&#xff09; - DC~5MHz, 150A DC&a…

使用案例总结Vlookup函数的30种用法

1 基础用法 =VLOOKUP(A12,B$1:D$8,3,0) 2 批量查找 =VLOOKUP(A11:A13,A2:C8,3,0) 3 模糊查找 =VLOOKUP("*"&D2&"*",A:B,2,0) 4 模糊查找2 =VLOOKUP(D10&"??",A:B,2,0) 5 模糊查找3 =

GAMES101-Lec10~12几何 曲线 曲面网格

目录 1.几何的表示1.1显示1.1.1更多显示表示方法1.1.1.1点云1.1.1.2多边形网格 1.2隐示1.2.1更多隐示表达法1.2.1.1代数曲面1.2.1.2 CSG1.2.1.3距离函数SDF1.2.1.4水平集1.2.1.5分型几何 2.曲线2.1贝塞尔曲线2.2 计算方法2.3代数表示2.4性质2.5逐段贝塞尔曲线 3.曲面3.1贝塞尔曲…

如何使用Java在Excel中添加动态数组公式?

本文由葡萄城技术团队发布。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 前言 动态数组公式是 Excel 引入的一项重要功能&#xff0c;它将 Excel 分为两种风格&#xff1a;Excel 365 和传统 …

C语言使用posix正则表达式库

在C语言中&#xff0c;你可以使用 POSIX 正则表达式库&#xff08;regex.h&#xff09;来进行正则表达式的模式匹配。POSIX 正则表达式库提供了一组函数来编译、执行和释放正则表达式。 下面是使用 POSIX 正则表达式库的基本步骤&#xff1a; 包含头文件 <regex.h>&…

C语言——完数难题(头歌编程刷题)

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 生命如同寓言&#xff0c;其价值不在于…

【react.js + hooks】基于事件机制的跨组件数据共享

跨组件通信和数据共享不是一件容易的事&#xff0c;如果通过 prop 一层层传递&#xff0c;太繁琐&#xff0c;而且仅适用于从上到下的数据传递&#xff1b;建立一个全局的状态 Store&#xff0c;每个数据可能两三个组件间需要使用&#xff0c;其他地方用不着&#xff0c;挂那么…

软件设计师——计算机组成原理(三)

&#x1f4d1;前言 本文主要是【计算机组成原理】——软件设计师——计算机组成原理的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 …

解决nuxt3报错:The requested module xxx does not provide an export named ‘PropType‘

现象如下&#xff1a; 从表象上就是typescript找不到PropType的类型声明 原因&#xff1a;这是vue3已知的type类型导入时存在的一个问题&#xff0c;而且一直没有得到解决 No matching export for import typescript interface Issue #2117 vitejs/vite GitHub 代码里面导…

人工智能强化学习:智能体自我进化的探索与挑战

导言 人工智能强化学习作为一种模仿人类学习方式的技术&#xff0c;近年来在机器学习领域取得了令人瞩目的进展。强化学习作为一种通过智能体与环境的交互学习的技术&#xff0c;与其他模型的融合不仅可以提升学习效果&#xff0c;还能在更广泛的领域中实现智能体的决策。本文将…

python学习,1.变量和简单的数据类型

一、编写文章的目的 1.这是为了初学者而写的&#xff0c;学习python比较简单然后上手&#xff0c;也会过滤一些&#xff0c;如果没有提起到的&#xff0c;可以在学习的时候进行补充 2.相对来说&#xff0c;上手难度不会很难。 二、内容 1.让首字母大写&#xff1b;字母都大写…

Linux查看进程PID以及杀掉进程的方法

目录 参考链接 前言 查看进程PID PS命令 ps -le命令 查找父进程 杀死进程 参考链接 【Linux 】 ps命令详解&#xff0c;查看进程pid_linux查看pid 对应的程序-CSDN博客 Linux查看进程PID的方法&#xff08;linux查进程的pid&#xff09;附带自动kill 掉_linux查看pid 对…

大数据分析与应用实验任务十二

大数据分析与应用实验任务十二 实验目的&#xff1a; 通过实验掌握spark机器学习库本地向量、本地矩阵的创建方法&#xff1b; 熟悉spark机器学习库特征提取、转换、选择方法&#xff1b; 实验任务&#xff1a; 一、逐行理解并参考编写运行教材8.3.1、8.3.3节各个例程代码…

linux脚本中 #!/bin/sh、#!/bin/bash

我们通常看到的脚本文件总是有以下这样的开头&#xff1a; #!/bin/bash本文解释一下这是什么&#xff0c;以及为什么要写它。 首先解释一下 #! &#xff0c;因为 #!有个专有的名词&#xff0c;叫 shebang 发音类似中文的 “蛇棒” 。为什么叫 shebang 呢&#xff1f; 首先 #…

【面向对象】C++/python/java的多态比较

一、面向对象的主要特点 封装&#xff1a;封装是把数据和操作数据的方法绑定在一起&#xff0c;对数据的访问只能通过已定义的接口。这可以保护数据不被外部程序直接访问或修改&#xff0c;增强数据的安全性。继承&#xff1a;继承是一种联结类的层次模型&#xff0c;并且允许…

1850_emacs_org-download在Windows上的使用

Grey 全部学习内容汇总&#xff1a; https://github.com/greyzhang/g_org 1850_emacs_org-download在Windows上的使用 对我来说&#xff0c;使用emacs很大的一个挑战是在Windows上&#xff0c;emacs的配置会比Linux上麻烦一些。而且&#xff0c;通常来说Windows上的体验会差…

SQL进阶理论篇(九):为什么不存在完美的索引

文章目录 简介索引片和过滤因子如何通过宽表避免回表什么是过滤因子理想索引设计&#xff1a;三星索引为什么很难存在理想的索引设计&#xff1f;参考文献 简介 本节将主要介绍以下部分&#xff1a; 什么是索引片&#xff0c;什么是过滤因子&#xff1f;设计索引的时候&#…