【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.mzph.cn/news/228967.shtml

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

相关文章

Mrdoc知识文档

MrDoc知识文档平台是一款基于Python开发的在线文档系统&#xff0c;适合作为个人和中小型团队的私有云文档、云笔记和知识管理工具&#xff0c;致力于成为优秀的私有化在线文档部署方案。我现在主要把markdown笔记放在上面&#xff0c;因为平时老是需要查询一些知识点&#xff…

mysql使用st_distance_sphere函数报错Incorrect arguments to st_distance_sphere

前言 最近使用空间点位查询数据时函数报错Incorrect arguments to st_distance_sphere报错。 发现问题 因为之前是没有问题的&#xff0c;所以把问题指向了数据&#xff0c;因为是外部数据&#xff0c;不是通过系统打点获取&#xff0c;发现是因为经纬度反了&#xff0c;loc…

软件测试指南

软件测试指南 软件集成测试软件系统测试&#xff08;功能性测试&#xff0c;性能测试&#xff09;

事件监听的艺术:掌握`addEventListener`的魅力

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

【Vulnhub 靶场】【IA: Keyring (1.0.1)】【中等】【20210730】

1、环境介绍 靶场介绍&#xff1a;https://www.vulnhub.com/entry/ia-keyring-101,718/ 靶场下载&#xff1a;https://download.vulnhub.com/ia/keyring-v1.01.ova 靶场难度&#xff1a;中等 发布日期&#xff1a;2021年07月30日 文件大小&#xff1a;1.1 GB 靶场作者&#xf…

基于Mamdani模糊神经网络的调速控制系统simulink建模与仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 模糊神经网络控制器概述 4.2 模糊神经网络控制器基本原理 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ............................…

UE5 项目设置

1、定义设置哪些参数 UCLASS(configEngine, globaluserconfig) class ADVMOD_API UGlobalSettings : public UObject {GENERATED_BODY()public:UGlobalSettings();const FString& GetPythonExePath() const { return PythonExePath.FilePath; }private:UPROPERTY(config, E…

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

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

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

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

记录 | Visual Studio报错:const char*类型的值不能用于初始化char*类型

Visual Studio 报错&#xff1a; const char *”类型的值不能用于初始化“char *”类型的实体错误 解决办法&#xff1a; 1&#xff0c;强制类型转换&#xff0c;例如&#xff1a; char * Singer::pv[] {(char*)"other", (char*)"alto", (char*)"c…

1848_emacs_org-mode代码块环境

Grey 全部学习内容汇总&#xff1a; https://github.com/greyzhang/g_org 1848_emacs_org-mode代码块环境 这一部分主要是涉及到一些代码的执行、引用以及输出处理等功能。从之前我看的资料来说&#xff0c;更加偏重于可重现研究但不一定是文学式编程的必要部分。 内容来源…

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

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

【小程序】-【

swiper、swiper-item轮播图 swiper是滑块视图容器。其中只可放置swiper-item组件。部分常用属性如下&#xff0c;其余属性详见&#xff1a;官方文档 <view class"banner"><swiperprevious-margin"30rpx"circularautoplayinterval"3000&q…

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

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

Ansible-playbook编译.yml脚本

1、playbook是什么&#xff1f; 在Ansible中&#xff0c;Playbook是用于配置、部署和管理被控节点的剧本。它由一个或多个play&#xff08;角色&#xff09;组成&#xff0c;每个play可以包含多个task&#xff08;台词&#xff0c;动作&#xff09;。使用Ansible的Playbook&am…

网络编程-认识套接字socket

文章目录 套接字概念端口号网络字节序 套接字类型流套接字数据报套接字 socket常见APIsocket函数bind函数listen函数accept函数connect函数sockaddr结构 套接字概念 socket套接字是进程之间一种通信机制&#xff0c;通过套接字可以在不同进程之间进行数据交流。在TCP/UDP中&…

如何开发一个prompt?prompt的使用有哪些原则?

提示词使用原则 如何开发一个跟自己预期结果接近的提示词&#xff1f;有哪些基本原则&#xff1f; 提示词迭代开发 写提示词时&#xff0c;第一次尝试是值得的&#xff0c;反复完善提示&#xff0c;获得越来越接近你想要的结果 原文来源于B站吴恩达提示工程教学公开课。…

ActionCLIP:A New Paradigm for Video Action Recognition

文章目录 ActionCLIP: A New Paradigm for Video Action Recognition动机创新点相关工作方法多模态框架新范式预训练提示微调 实验实验细节消融实验关键代码 总结相关参考 ActionCLIP: A New Paradigm for Video Action Recognition 论文&#xff1a;https://arxiv.org/abs/21…

服务端主动给客户端发消息?实战教学:使用Nestjs实现服务端推送SSE

前言 服务端消息推送SSE是常用的服务器消息通信手段&#xff0c;适用于服务器主动给客户端发送消息的场景&#xff0c;例如私信通知&#xff0c;扫描登录等都可以使用SSE实现。SSE的底层原理是客户端与服务端建立 HTTP 长链接。 Nestjs 框架内置了对SSE的支持&#xff0c;本文…

ES6 面试题 | 15.精选 ES6 面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…