【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.hqwc.cn/news/282413.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

navicat连接mysql报错过程以及解决

1.刚开始报错如下图 于是我利用这段报错信息&#xff08;2059 - Authentication plugin caching sha2 password cannot be loaded&#xff09;百度。 1.1上面报错的原因和解决过程 百度说是mysql的加密方式不对&#xff0c;如下图 所以这里进入数据库&#xff0c;修改mysql这…

缓存击穿的原因和解决方案

缓存击穿 原因&#xff1a;一个被高并发访问并且缓存重建业务较复杂的key突然失效了&#xff0c;无数的请求访问会在瞬间给数据库带来巨大的冲击 解决方案 1.互斥锁 优点 没有额外的内存消耗保证一致性实现简单 缺点 线程需要等待&#xff0c;性能受影响可能有死锁风险 …

使用下载代替物理串口输出-STM32 Debug (printf) Viewer

使用下载代替物理串口输出-STM32 Debug 硬件要求配置方法代码要求打印输出结果 硬件要求 STM32的PB9、PB10引脚的串口1通常用作其他功能使用后&#xff0c;无法通过printf()函数打印输出想要调试输出查看变量或调试信息。现已使用另外一种方法实现printf()函数打印输出。 ST…

EasyExcel实现⭐️本地excel数据解析并保存到数据库的脚本编写,附案例实现

目录 前言 一、 EasyExcel 简介 二、实战分析 1.Controller控制层 2. service方法和方法实现 3.EasyExcel相关类 3.1 excel表实体类 3.2 自定义监听器类 4.测试 4.1 准备工作 4.2 断点调试 5.生成脚本文件 三、分析总结 章末 小伙伴们大家好&#xff0c;最近开发的时…

JavaSE第7篇:封装

文章目录 一、封装1、好处:2、使用 二、四种权限修饰符三、构造器1、作用2、说明3、属性赋值的过程 一、封装 封装就是将类的属性私有化,提供公有的方法访问私有属性 不对外暴露打的私有的方法 单例模式 1、好处: 1.只能通过规定的方法来访问数据 2.隐藏类的实例细节,方便…

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

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

深度学习项目实战:垃圾分类系统

简介&#xff1a; 今天开启深度学习另一板块。就是计算机视觉方向&#xff0c;这里主要讨论图像分类任务–垃圾分类系统。其实这个项目早在19年的时候&#xff0c;我就写好了一个版本了。之前使用的是python搭建深度学习网络&#xff0c;然后前后端交互的采用的是java spring …

python学习2

大家好&#xff0c;这里是七七&#xff0c;这次学习的例子是一个数据清洗代码。完整代码在最后。 开始这次的内容 目录 代码一 代码二 代码三 代码四 全部代码 代码一 xlsx_file data/附件1.xlsx df_1 pd.read_excel(xlsx_file) xlsx_file data/附件2.xlsx df pd.re…

workflow系列教程(4)Parallel并联任务流

往期教程 如果觉得写的可以,请给一个点赞关注支持一下 观看之前请先看,往期的博客教程,否则这篇博客没办法看懂 workFlow c异步网络库编译教程与简介 C异步网络库workflow入门教程(1)HTTP任务 C异步网络库workflow系列教程(2)redis任务 workflow系列教程(3)Series串联任务流…

一个 tomcat 下如何部署多个项目?附详细步骤

一个tomcat下如何部署多个项目&#xff1f;Linux跟windows系统下的步骤都差不多&#xff0c;以下linux系统下部署为例。windows系统下部署同理。 1 不修改端口&#xff0c;部署多个项目 清楚tomcat目录结构的应该都知道&#xff0c;项目包是放在webapps目录下的&#xff0c;那…

【SpringBoot零基础入门到项目实战①】解锁现代Java开发之门:深度探究Spring Boot的背景、目标及选择理由

文章目录 引言Spring Boot的背景和目标背景目标 为什么选择Spring Boot1. 简化配置2. 内嵌式容器3. 生态系统支持4. 大量的Starter5. 广泛的社区支持6. 适用于微服务架构7. 丰富的扩展机制 实例演示创建一个简单的Spring Boot应用 拓展与深入学习1. Spring Boot Actuator2. Spr…

MySQL数据库,表的增量备份与恢复

1. 从物理与逻辑的角度 数据库备份可以分为物理备份和逻辑备份。物理备份是对数据库操作系统的物理文件&#xff08;如数据 文件&#xff0c;日志文件等&#xff09;的备份。这种类型的备份适用于在出现问题时需要快速恢复的大型重要数据库。 物理备份又可以分为冷备份&#xf…