qsort函数的模拟实现

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


导言

大家好,很高兴又和大家见面了!!!

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

一、回调函数

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

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

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

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

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

二、冒泡排序

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

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

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

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

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

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

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;
			}
		}
	}
}

我们来看一下排序的效果:

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

冒泡排序的排序逻辑不难,就是两个元素相邻的元素比较,直到没有元素需要交换为止;

需要比较的总趟数比元素个数少一;

每一趟排序的次数,要比前一趟少一;

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

三、qsort函数

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

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

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

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

我们继续往下看:

【C语言加油站】qsort函数的模拟实现_冒泡排序_05

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

  1. 比较函数是用户自己提供的,函数有两个无类型指针型的参数;
  2. 函数的返回值需要按照以下标准:
  • 当参数1<参数2时,返回值>0;
  • 当参数1=参数2时,返回值=0;
  • 当参数1>参数2时,返回值<0;
  1. 通过这个比较函数的返回值,我们可以得到一个递增的数组;
  2. 当我们需要得到一个递减的数组时,需要将参数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;
}

下面我们就来测试一下:

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

可以看到,此时我们通过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函数测试

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

【C语言加油站】qsort函数的模拟实现_void*指针_07

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

五、知识点总结

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

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

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

结语

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

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