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

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.mfbz.cn/a/252864.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单片机控制步进电机

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、步进电机的工作原理是什么&#xff1f;二、连线图三、程序1.参考程序2.实际测试 四、开发板1.原理图2.实际连接图3.参考程序4.测试5. 思考 五、步距角总结 …

面向对象三大特征——继承

目录 1. 概述 2. 继承的限制 2.1 单继承 2.2 访问修饰符 2.3 . final 3. 重写 4. super 4.1super的作用 4.2访问父类的成员和被重写方法 4.3调用父类的构造器 1. 概述 多个类中存在相同属性和行为时&#xff0c;将这些内容抽取到单独一个类中&#xff0c;那么就无需在…

【JavaEE】多线程案例 - 定时器

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文于《JavaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&…

中文论文修改和润色哪个好 神码ai

大家好&#xff0c;今天来聊聊中文论文修改和润色哪个好&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff0c;可以借助此类工具&#xff1a; 标题&#xff1a;中文论文修改和润色&#xff0c;哪个更好&#x…

【OpenHarmony 北向应用开发】ArkTS语言入门(构建应用页面)

ArkTS语言入门 在学习ArkTS语言之前&#xff0c;我们首先需要一个能够编译并运行该语言的工具 DevEco Studio。 了解ArkTS ArkTS是OpenHarmony优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript&#xff08;简称TS&#xff09;生态基础上做了进一步扩展&#xff0c;继…

关于找不到XINPUT1_3.dll,无法继续执行代码问题的5种不同解决方法

一、xinput1_3.dll的作用 xinput1_3.dll是Windows操作系统中的一款动态链接库文件&#xff0c;主要用于支持游戏手柄和游戏输入设备。这款文件属于Microsoft Xbox 360兼容性库&#xff0c;它包含了与游戏手柄和其他输入设备相关的功能。在游戏中&#xff0c;xinput1_3.dll负责…

(数据结构)单链表的插入删除

代码实现 #include<stdio.h> #include<stdlib.h> typedef struct LNode {int data;struct LNode* next; }LNode, * LinkList; //创建头结点 LNode* InitList(LinkList L) {L (LNode*)malloc(sizeof(LNode));if (L NULL){printf("申请头结点失败\n");…

git 上传大文件操作 lfs 的使用

我们要先去下载 下载后安装 我最后还是下载到了D:\git\Git\bin这个目录下 如何检查是否下载成功呢&#xff0c;用 git lfs install 在命令行运行就可以查看 下面怎么上传文件呢 首先我们还是要初始化文件的 git init 下一步输入命令 git lfs install 下一步 git lfs tra…

如何在安装了巨魔2的iphone中运行Theos编译的本地化二进制工具:Bootstrap

如何在安装了巨魔2的iphone中运行Theos编译的本地化二进制工具:Bootstrap 一、首先从https://github.com/34306/iPA/releases/tag/bstr下载jb.zip、jb_with_jb_folder.zip、prefs_fix.ipa三个文件。 二、然后使用Filza文件管理器把jb.zip解压后复制到/var/containers/jb目录&…

1836_emacs显示空白字符

Grey 全部学习汇总&#xff1a; GitHub - GreyZhang/editors_skills: Summary for some common editor skills I used. 全部学习内容汇总&#xff1a; 1836_emacs显示空白字符 show-trailing-whitespace是emacs中内置的一个变量&#xff0c;这个变量的值如果设置为nil那么不…

c++ websocket 协议分析与实现

前言 网上有很多第三方库&#xff0c;nopoll,uwebsockets,libwebsockets,都喜欢回调或太复杂&#xff0c;个人只需要在后端用&#xff0c;所以手动写个&#xff1b; 1:环境 ubuntu18 g(支持c11即可) 第三方库:jsoncpp,openssl 2:安装 jsoncpp 读取json 配置文件 用 自动安装 网…

【案例】注册表简介,新建一个右键菜单打开方式选项

这里写目录标题 来源注册表的介绍注册表编辑器VScode的打开方式菜单![image-20231217201730121](https://img-blog.csdnimg.cn/img_convert/56c02643df9e8ec3afb4f3ac5cc0cdd5.png)如何自定义一个右键菜单备份注册表新建一个菜单选项”右键用记事本打开“ DWORDQWORD可扩充字符…

社交网络分析3:社交网络隐私攻击、保护的基本概念和方法 + 去匿名化技术 + 推理攻击技术 + k-匿名 + 基于聚类的隐私保护算法

社交网络分析3&#xff1a;社交网络隐私攻击、保护的基本概念和方法 去匿名化技术 推理攻击技术 k-匿名 基于聚类的隐私保护算法 写在最前面社交网络隐私泄露用户数据暴露的途径复杂行为的隐私风险技术发展带来的隐私挑战经济利益与数据售卖防范措施 社交网络 用户数据隐私…

力扣刷题-二叉树-路径总和

112 路径总和 给定一个二叉树和一个目标和&#xff0c;判断该树中是否存在根节点到叶子节点的路径&#xff0c;这条路径上所有节点值相加等于目标和。 说明: 叶子节点是指没有子节点的节点。 示例: 给定如下二叉树&#xff0c;以及目标和 sum 22&#xff0c; 返回 true, 因为…

【漏洞复现】红帆OA iorepsavexml.aspx文件上传漏洞

漏洞描述 广州红帆科技深耕医疗行业20余年,专注医院行政管控,与企业微信、阿里钉钉全方位结合,推出web移动一体化办公解决方案——iOffice20(医微云)。提供行政办公、专业科室应用、决策辅助等信息化工具,采取平台化管理模式,取代医疗机构过往多系统分散式管理,实现医…

C#实现MQTT over WebSocket

如何在网页端实现MQTT消息的发布和订阅&#xff1f; 实现MQTT功能&#xff0c;可以发布和订阅主题通过WebSocket协议将MQTT消息转发给对应的网页端 带着这个实现思路&#xff0c;采用C#控制台程序实现MQTT服务端功能&#xff0c;web端可以直接使用websocket插件与服务端双向通…

力扣刷题-二叉树-二叉树的所有路径

257 二叉树的所有路径 给定一个二叉树&#xff0c;返回所有从根节点到叶子节点的路径。 说明: 叶子节点是指没有子节点的节点。 示例: 思路 参考&#xff1a; https://www.programmercarl.com/0257.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE…

MNIST内置手写数字数据集的实现

torchvision库 torchivision库是PyTorch中用来处理图像和视频的一个辅助库&#xff0c;接下来我们就会使用torchvision库加载内置的数据集进行分类模型的演示 为了统一数据加载和处理代码&#xff0c;PyTorch提供了两个类用于处理数据加载&#xff0c;他们分别是torch.utils.…

进程通信知识基础【Linux】——下篇

目录 前文 一&#xff0c;命名管道 创建命名管道 1. getline——c库 2. unlink——系统接口 实践代码 common.hpp client.cpp server.cpp Log.cpp 二&#xff0c;共享内存&#xff08;system V接口&#xff09; 1. 创建共享内存 shmget接口 2. 删除共享内存 常见…

PMP项目管理 - 相关方管理

系列文章目录 PMP项目管理 - 质量管理 PMP项目管理 - 采购管理 PMP项目管理 - 资源管理 PMP项目管理 - 风险管理 PMP项目管理 - 沟通管理 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream weaving wing…
最新文章