排序之玩转qsort函数——【C语言】

简介: 说起排序,我们会想起许多算法,在之前的博客中我也写到过,比如:冒泡排序法、快速排序法、选择排序法等等。其实在C语言中一直有一个可以将数组中的内容进行排序的函数且功能完善内容齐全的库函数——qsort函数。今天就让我们来探索一下吧!

说起排序,我们会想起许多算法,在之前的博客中我也写到过,比如:冒泡排序法、快速排序法、选择排序法等等。其实在C语言中一直有一个可以将数组中的内容进行排序的函数且功能完善内容齐全的库函数——qsort函数。今天就让我们来探索一下吧!


回调函数


在了解qsort函数时,我们先来说一下什么时回调函数。


回调函数:


回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当 这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调 用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。


回调函数其实是对某一类函数的一种称呼,在使用时我更感觉类似一种嵌套关系,在对应的时间使用回调函数就会有意想不到的收获。


在使用qsort函数时我们会用到回调函数,所以qsort函数可以在更广泛的常见进行运用(整型、浮点型、字符串、结构体等等)。接下来就让我们走进qsort函数中!


初始qsort函数


qsort库函数是在#include<stdlib.h>中的,作用就是对数组中的元素进行排序。


qsort函数的特点:


1.使用的是快速排序的方法


2.适合于任何类型数据的排序


59e7f0066c0546b2b9a605f0596c67b6.png


以上就是qsort函数的返回值与各个参数的内容,现在我们进行分析:


返回值:void(不返回任何内容)


参数:


void* base:指向要排序的数组的第一个对象的指针,转换为 void *


size_t num:数组中由base指向的元素数。 size_t是无符号整型。


size_t size:数组中每个元素的大小(以字节为单位)size_t是无符号整型。


int (*compar) (const void*, const void*):这是一个函数指针,实参应该给其一个函数的地址,其中给予的函数的返回值为int,两个参数是指向比较两个元素的函数的指针。


40d92463d8c6481e80d8096b14fd76ff.png


所以在使用qsort函数时,我们应该创建一个比较的函数传入qsort函数中让其使用。 创建的比较函数应该与自己想要比较的数组内容进行匹配,对应不同的数组内容,我们应该创建不同的比较函数进行比较。


如何创建比较函数


在创建自己想要比较的compar函数时,最重要的一点时遵循qsort参数的模板进行创建,否则qsort函数将无法使用。


void*(空指针):它可以接收任何类型的指针变量,但是不能进行指针操作,因为它没用空间访问权限,如果没有注意此情况编译器就会报错。所以我们在给予赋值时必须将它强制类型转换,转换为自己需要的数据类型即可。


当排序数组为整型数组:


int compar(const void* p1, const void* p2)
{
  return (*(int*)p1 - *(int*)p2);
}


排序数组为字符串数组时,我们应该使用#include<string.h>中的strcmp函数进行比较:


int compar(const void* p1, const void* p2)
{
  return (strcmp(*(char*)p1 , *(char*)p2));
}


结构体数组时,比较内容为字符串时:


int compar(const void* p1, const void* p2)
{
  return strcmp((struct stu*)p1->name , (struct stu*)p2->name);
}


以上是几个比较典型的例子,看完这些例子我相信已经知道compar函数应该怎么创建了,下面让我们来上一段代码进行实操。


qsort函数的引用


根据上面的说明,我们基本已经掌握qsort函数的基本使用方法,现在让我们通过代码对qsort函数有更进一步的认识。


#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct Stu
{
  char name[20];
  int age;
};
void print(int arr[10], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
}
int compar1(const void* p1, const void* p2)
{
  return (*(int*)p1 - *(int*)p2);
}
int compar2(const void* p1, const void* p2)
{
  return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void test1()
{
  int arr1[10] = { 3,1,5,2,9,7,4,8,0,6 };
  int sz = sizeof(arr1) / sizeof(arr1[0]);
  qsort(arr1, sz, sizeof(arr1[0]), compar1);
  print(arr1, sz);
}
void test2()
{
  struct Stu arr2[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu",16} };
  int sz = sizeof(arr2) / sizeof(arr2[0]);
  qsort(arr2, sz, sizeof(arr2[0]), compar2);
  for (int i = 0; i < sz; i++)
  {
    printf("\n%s %d", arr2[i].name, arr2[i].age);
  }
}
int main(void)
 {
  test1();
  test2();
  return 0;
}


这段代码是将一个整数数组和结构体数组中的字符串进行升序排序。这里我想说一下字符之间的比大小是通过它们的ASCII码值进行比较。运行结果如下:


926bca9cf58c4d94a03a5f713ba1a2d4.png


如果想要进行降序排序,我们只需要将compar函数中的比较的两个值进行交换即可。


84185936f6e9457c87acbcab02f9c703.png

f2ceb51cc7ee4a079a325c8944f9d8cd.png


模拟qsort函数


根据我们在使用qsort函数的一些功能特点,来模拟出qsort函数的功能。


qsort函数的功能就是排序,所以这个函数中一定有某种排序的方法,在这里我们将使用冒泡排序法进行。(其他排序方法也是可以的)


void bubble_sort(int arr[], int sz)
{
  int i = 0; 
  for (i = 0; i < sz - 1; i++)
  {
    int j = 0;
    for (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;
      }
    }
  }
}


这是一个最标准的冒泡排序法,我们通过上面的排序代码为框架进行模拟qsort函数。


假设就使用这个函数当作qsort函数,我们就会发现很多问题!!!


问题一:参数只能接收整型数组,其他数组不能被接收。


解决:我们就可以模仿qsort函数中的第一个参数,将第一个参数改为void* base,这样什么类型的数组我们都可以接收。


问题二:当我们使用void*base作为第一个参数时,指针的移动应该怎么实现?(数组元素的大小和数量)


解决:继续仿照qsort函数,加入size_t num和size_t size.


问题三:上述函数中两数两两比较是建立于整数基础上的比较,所以使用> < >= <=就可以实现。如果传入的数组为结构体数组时,我们就不能用比较整数的比较方法实现了。(对于不同类型的数据不能简单的使用>进行比较)


解决:我们就需要用到回调函数,将两个元素的比较方法,以函数参数的形式传递。(函数指针将成为第四个参数)


当创建好compar函数时,我们应该怎么去给compar函数去传递参数呢?


compar函数是多个函数,通过不同的调用我们可以比较不同的数组,但是在模拟函数中给compar函数传参的位置只有一个,我们应该考虑兼顾所以的数组类型。


我们知道了数组中每个元素的大小,相当于我们知道其元素在内存中所占的大小,为了兼顾所有数据,我们用访问内存权限最小的指针char*来作为基本单位,在乘上每个元素的大小,就可以实现任何数据的访问。



5294a4e481374b11b7c29c312884f807.png

f6a53262c47847ad9539e3410ea22028.png

在for循环中使用即可访问数组中的所有元素。


当传入的元素在compar函数中比较如果大于0,就要进行元素内容交换。又有问题出现了,传入的数据类型不同,交换的内容大小不同,为了其中的兼容性,我们可以继续沿用上述思想,创建一个swap交换函数,传入刚才compar函数使用的两个函数地址和数组一个元素所占内存大小size即可。


5f13452f35a449d1a1c82a94b32651eb.png


swap函数会将两个数据中的数据一个字节一个字节的交换,这也可以保证所有类型数组都可以进行。


所有的准备工作已经完成,下面让我们完成qsort函数的模拟。


#include<stdio.h>
#include<string.h>
void print(int arr[10], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
}
void swap(char*p1,char*p2,int size)
{
  int i = 0;
  char tmp = 0;
  for (i = 0; i < size; i++)
  {
    tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
    p1++;
    p2++;
  }
}
int compar(const void* p1, const void* p2)
{
  return *(int*)p1 - *(int*)p2;
}
void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
  int i = 0; 
  for (i = 0; i < num-1; i++)
  {
    for (int j = 0; j < num - 1 - i; j++)
    {
      if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
      {
        swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
      }
    }
  }
}
void test1()
{
  int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bubble_sort(arr, sz, sizeof(arr[0]), compar);
  print(arr, sz);
}
int main(void)
{
  test1();
}


如果想加入其他类型数组进行排序,只需要加入新的compar函数和要比较的内容即可,如果想要降序排序,只需将if函数中>0改为<0即可。下面是成功运行的结果:


3e14a4173f8943f2bd04f30ab8852d9b.png


模拟的qsort函数主体为bubble_sort函数和swap函数:


void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
  int i = 0; 
  for (i = 0; i < num-1; i++)
  {
    for (int j = 0; j < num - 1 - i; j++)
    {
      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(char*p1,char*p2,int size)
{
  int i = 0;
  char tmp = 0;
  for (i = 0; i < size; i++)
  {
    tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
    p1++;
    p2++;
  }
}
目录
相关文章
|
22天前
|
存储 编译器 C语言
爱上C语言:函数递归,青蛙跳台阶图文详解
爱上C语言:函数递归,青蛙跳台阶图文详解
|
22天前
|
编译器 程序员 C语言
爱上C语言:什么C语言函数不太会,那你千万不要错过这篇(函数篇)
爱上C语言:什么C语言函数不太会,那你千万不要错过这篇(函数篇)
|
8天前
|
C语言
C语言:内存函数(memcpy memmove memset memcmp使用)
C语言:内存函数(memcpy memmove memset memcmp使用)
|
2天前
|
缓存 安全 编译器
【C 言专栏】C 语言函数的高效编程技巧
【5月更文挑战第1天】本文探讨了C语言中函数的高效编程技巧,包括函数的定义与作用(如代码复用和提高可读性)、设计原则(单一职责和接口简洁)、参数传递方式(值传递、指针传递和引用传递)、返回值管理、调用约定、嵌套与递归调用,以及函数优化技巧和常见错误避免。掌握这些技巧能提升C语言代码的质量和效率。
【C 言专栏】C 语言函数的高效编程技巧
|
4天前
|
C语言
pta浙大版《C语言程序设计(第3版)》 习题6-4 使用函数输出指定范围内的Fibonacci数 (20分)
pta浙大版《C语言程序设计(第3版)》 习题6-4 使用函数输出指定范围内的Fibonacci数 (20分)
|
4天前
|
C语言
pta 浙大版《C语言程序设计(第3版)》题目集 习题6-6 使用函数输出一个整数的逆序数 (20分)
pta 浙大版《C语言程序设计(第3版)》题目集 习题6-6 使用函数输出一个整数的逆序数 (20分)
|
4天前
|
C语言
(浙大版《C语言程序设计(第3版)》 习题6-5 使用函数验证哥德巴赫猜想 (20分)
(浙大版《C语言程序设计(第3版)》 习题6-5 使用函数验证哥德巴赫猜想 (20分)
|
7天前
|
安全 C语言
【C语言】strcpy与strncpy函数的使用和模拟实现
【C语言】strcpy与strncpy函数的使用和模拟实现
5 0
|
7天前
|
C语言
【C语言】字符分类函数与字符转换函数
【C语言】字符分类函数与字符转换函数
9 1
|
7天前
|
程序员 编译器 C语言
C语言之函数与参数
C语言之函数与参数
5 0