C语言:qsort模拟实现
在C语言编程中,我们经常需要对数组进行排序。如果只是对整型数组排序,可以直接写一个排序函数。但如果既要对整型数组排序,又要对浮点型数组排序,难道要写两个几乎一样的函数吗?为了解决这个问题,C语言提供了通用的排序函数 qsort。
qsort函数是C语言中的一个标准函数,用于对数组进行快速排序。其函数原型如下:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
参数解释:
base:指向数组首元素的指针。nmemb:数组的元素个数。size:每个元素的大小(以字节为单位)。compar:比较函数的函数指针。
compar函数用于定义元素之间的比较规则。它需要返回一个整数值,表示两个元素之间的关系:
- 如果
*ptr1 < *ptr2,则返回一个负整数。 - 如果
*ptr1 == *ptr2,则返回0。 - 如果
*ptr1 > *ptr2,则返回一个正整数。
使用示例:
int compare(const void *a, const void *b)
{
return (*(int *)a - *(int *)b);
}
int main()
{
int arr[] = {5, 2, 8, 10, 1};
int n = sizeof(arr) / sizeof(arr[0]);
// 排序前打印
printf("排序前:");
for(int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
// 使用qsort排序
qsort(arr, n, sizeof(int), compare);
// 排序后打印
printf("排序后:");
for(int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
运行结果:
排序前:5 2 8 10 1
排序后:1 2 5 8 10
qsort函数是C标准库中提供的用于快速排序的函数。通过传入一个比较函数,可以对任意类型数组进行排序。使用前需要引入
<stdlib.h>头文件。
也许你现在有点迷惑,为什么这个qsort函数用起来这么麻烦?接下来通过模拟实现一个qsort函数来理解它的设计思路。
模拟实现思路
鉴于大部分同学学习指针的时候,还没有学习过快速排序,所以这里用冒泡排序进行代替。先从一个最基础的冒泡排序开始:
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void bubble(int* arr, int sz)
{
for (int i = 0; i < sz; i++)
{
for (int j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
swap(&arr[j], &arr[j + 1]);
}
}
}
}
这个基础版本的排序函数有两个明显的缺陷:
- 只能排
int类型的数据 - 只能排升序
要实现一个通用的排序函数,需要解决这两个问题。接下来一步步来改造这个函数。
Swap - 通用交换函数
首先要解决的是交换函数。如果要交换不同类型的数据(比如int、double、结构体等),每种数据占用的字节可能都不同,该如何实现?
想要在函数内部实现数据交换,需要传址调用。但是Swap函数要如何接收各种数据类型的指针?
答案是使用void*类型的指针。void*类型的指针可以接收任何类型的指针,但有一个缺点就是不能直接解引用。
那么如何交换数据?可以换个思路:
- 用
void*接收待交换数据的指针 - 一个字节一个字节地交换数据
- 把指针转换为
char*类型(因为char表示一个字节,所有数据类型都以字节为单位) - 通过参数传入数据类型的大小,知道要交换多少个字节
也就是说,Swap函数需要知道两个变量的指针,以及变量所占的字节数。只是单纯的交换两个变量的内存,并不知道变量的类型。
思路理清后,看看Swap需要的参数:
void Swap(void* p1, void* p2, int size)
p1和p2:待交换的两个变量的指针size:待交换的变量的类型所占字节数
代码实现:
void Swap(void* p1, void* p2, int size)
{
for (int i = 0; i < size; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
Compare - 通用比较函数
接下来要解决的是比较问题。不同类型的数据比较规则往往不同:
- 整数比较大小
- 字符串比较字典序
- 有时需要升序,有时需要降序
为了解决这个问题,qsort的设计者采用了一个巧妙的方法:让用户自己定义比较规则。
这就用到了回调函数的概念:用户把比较规则写在一个函数里,然后把这个函数通过函数指针,作为参数传给排序函数。这个比较函数一般命名为cmp。
比较函数的规则很简单:
- 如果第一个数应该排在第二个数前面,返回负数
- 如果两个数相等,返回0
- 如果第一个数应该排在第二个数后面,返回正数
函数指针做参数有一个缺点,那就是指针类型是写死的。
如果用户要比较两个int类型数据,用户会这样写:int cmp(int* x, int* y);。此函数的指针类型为int (*)(int*, int*);。
但是如果比较float的数据,那就会这样写:int cmp(float* x, float* y);。此函数的指针类型为int (*)(float*, float*);。
函数指针类型不一样,这不利于统一处理函数。那么如何让cmp函数可以接收任何类型的数据呢?此时void*类型的指针又上场了,为了避免数据类型问题,直接传void*指针来统一处理数据。
于是qsort规定:所有人自己在写cmp函数的时候,两个待比较的参数必须是void*类型。返回值必须是int类型。
至此,就可以确定回调函数指针类型就是int (*)(void*, void*)了。
看到以下cmp函数:
int cmp(void* e1, void* e2)
{}
假设要比较两个int类型的数据,现在我们得到了两个void*类型的指针,分别指向两个数据,要如何比较?
答案当然是:先强制转化为int*类型的指针,解引用,然后再比较。
比如这样:
int int_cmp(void* e1, void* e2)
{
int ret = *(int*)e1 - *(int*)e2;
return ret;
}
(int*)e1是在对void*类型的指针进行类型转化,转化完成后再解引用,所以是*(int*)e1。e2同理。
依据qsort对返回值的规定,直接让两数相减即可。如果你想排逆序,那么交换e1和e2的位置即可:
int int_cmp(void* e1, void* e2)
{
return *(int*)e2 - *(int*)e1;//逆序排序
}
排序主体实现
有了通用的交换函数和比较函数,现在可以实现完整的排序函数了:
看看最终版本的qsort的参数:
void qsort(void* base, int nmemb, int size,
int(*cmp)(const void*, const void*))
base:即传入的待排序数组,用void*接收,以适应不同类型数据nmemb:这个数组的元素个数,防止越界访问size:待排序的变量类型,所占用的字节数(Swap需要知道这个变量占几个字节)cmp:即用户自定义的比较函数,这里统一了类型为int (*)(void*, void*),用户使用时要用这种类型的函数
先前已经优化了用于比较的cmp函数,接下来就是把原来函数种比较的大于小于号,改成利用cmp函数比较:
原本的比较:
if (arr[j] > arr[j + 1])
现在有指向数组的指针base,指针偏移量j,单个元素占用的字节数size,以及用于比较的函数cmp。
首先要定位到当前j指向的元素,一般对于指针的访问,直接*(base + j)即可,但是此处base是void*类型的指针,不能解引用。与上一次相同,既然知道了一个变量占几个字节,干脆都统一为cahr*类型,一个一个字节来处理。
所以要先把base转化为char*:(cahr*)base。随后跳过j个变量,一个变量占size个字节,那就需要跳过j * size个字节,因此目标元素的地址为:(char*)base + (j * size)。
现在要往cmp函数里面传入j和j + 1位置的元素的地址,所以cmp的调用为:
cmp((char*)base + j * size, (char*)base + ((j + 1) * size))
如果cmp的返回值大于0,则说明要交换,if语句如下:
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
//Swap
}
如果满足要求,需要利用Swap交换,传参与cmp是同理的:
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
最后看一下完整的代码:
void Swap(void* p1, void* p2, int size)
{
for (int i = 0; i < size; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void qsort(void* base, int nmemb, int size, int(*cmp)(const void*, const void*))
{
for (int i = 0; i < nmemb - 1; i++)
{
for (int j = 0; j < nmemb - i - 1; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
通过以上的模拟实现过程,可以深入理解qsort函数的设计思想。它的精妙之处在于:
- 使用
void*指针实现了对任意类型数据的支持 - 通过回调函数机制,让用户自定义比较规则
- 按字节进行数据交换,实现了类型无关的数据操作
本文详细介绍了C语言标准库中的qsort函数,通过冒泡排序模拟和自定义cmp函数,展示了如何根据不同数据类型和排序需求对数组进行排序。重点讲解了Swap交换函数、cmp比较函数和qsort排序主体的实现细节。
4853

被折叠的 条评论
为什么被折叠?



