C语言:qsort模拟实现

本文详细介绍了C语言标准库中的qsort函数,通过冒泡排序模拟和自定义cmp函数,展示了如何根据不同数据类型和排序需求对数组进行排序。重点讲解了Swap交换函数、cmp比较函数和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]);
            }
        }
    }
}

这个基础版本的排序函数有两个明显的缺陷:

  1. 只能排int类型的数据
  2. 只能排升序

要实现一个通用的排序函数,需要解决这两个问题。接下来一步步来改造这个函数。


Swap - 通用交换函数

首先要解决的是交换函数。如果要交换不同类型的数据(比如intdouble、结构体等),每种数据占用的字节可能都不同,该如何实现?

想要在函数内部实现数据交换,需要传址调用。但是Swap函数要如何接收各种数据类型的指针?

答案是使用void*类型的指针。void*类型的指针可以接收任何类型的指针,但有一个缺点就是不能直接解引用

那么如何交换数据?可以换个思路:

  1. void*接收待交换数据的指针
  2. 一个字节一个字节地交换数据
  3. 把指针转换为char*类型(因为char表示一个字节,所有数据类型都以字节为单位)
  4. 通过参数传入数据类型的大小,知道要交换多少个字节

也就是说,Swap函数需要知道两个变量的指针,以及变量所占的字节数。只是单纯的交换两个变量的内存,并不知道变量的类型。

思路理清后,看看Swap需要的参数:

void Swap(void* p1, void* p2, int size)
  • p1p2:待交换的两个变量的指针
  • 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*)e1e2同理。

依据qsort对返回值的规定,直接让两数相减即可。如果你想排逆序,那么交换e1e2的位置即可

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)即可,但是此处basevoid*类型的指针,不能解引用。与上一次相同,既然知道了一个变量占几个字节,干脆都统一为cahr*类型,一个一个字节来处理。

所以要先把base转化为char*(cahr*)base。随后跳过j个变量,一个变量占size个字节,那就需要跳过j * size个字节,因此目标元素的地址为:(char*)base + (j * size)

现在要往cmp函数里面传入jj + 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函数的设计思想。它的精妙之处在于:

  1. 使用void*指针实现了对任意类型数据的支持
  2. 通过回调函数机制,让用户自定义比较规则
  3. 按字节进行数据交换,实现了类型无关的数据操作

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值