排序之玩转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++;
  }
}
目录
相关文章
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
36 3
|
1天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
22 6
|
18天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
31 6
|
21天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
65 8
|
21天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
59 7
|
2月前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
43 10
|
1月前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
2月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
64 7
|
2月前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
32 4
|
2月前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。