排序与搜索:qsort、bsearch 与自定义比较函数

排序和搜索,是 C 语言命令行工具里最常用的两个操作。你想想看,不管是处理日志文件、解析配置数据,还是做内存数据库的索引,都离不开它们。今天我就把这两个家伙彻底讲透。

一、qsort:C 标准库的排序利器

qsort 是 C 标准库提供的快速排序实现。名字里的 "q" 就是 "quick",快速。它定义在 <stdlib.h> 里,原型长这样:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

参数看着多,其实就四个:

  • base:待排序数组的首地址
  • nmemb:数组元素个数
  • size:每个元素的大小(字节数)
  • compar:比较函数的指针

嗯,这里有个关键点——比较函数。qsort 不知道你要排整数还是排字符串,它只负责交换内存块。怎么比,得你告诉它。

核心原则:比较函数返回负数表示 a < b,正数表示 a > b,0 表示相等。

整数排序示例

#include <stdio.h>
#include <stdlib.h>

int cmp_int(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    // 升序:ia - ib
    // 降序:ib - ia
    return ia - ib;
}

int main() {
    int arr[] = {34, 7, 23, 32, 5, 62};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, n, sizeof(int), cmp_int);

    for (size_t i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

输出:5 7 23 32 34 62

避坑指南:我曾经在比较函数里直接写 return *(int*)a - *(int*)b;,结果遇到 INT_MIN 和 INT_MAX 时溢出了。所以,稳妥的做法是拆开赋值,或者用 if (ia > ib) return 1; else if (ia < ib) return -1; else return 0;

字符串排序示例

int cmp_str(const void *a, const void *b) {
    // a 和 b 是 char** 类型,因为数组元素是 char*
    const char *sa = *(const char **)a;
    const char *sb = *(const char **)b;
    return strcmp(sa, sb);
}

// 调用
char *words[] = {"banana", "apple", "cherry", "date"};
qsort(words, 4, sizeof(char *), cmp_str);

这里容易犯的错是:把 const char ** 写成了 const char *。记住,数组元素是指针,所以比较函数收到的参数是指向指针的指针。

二、bsearch:二分查找的正确姿势

排序之后,自然就是查找。bsearch 是二分查找的标准实现,前提是数组必须已经排好序。

void *bsearch(const void *key, const void *base,
              size_t nmemb, size_t size,
              int (*compar)(const void *, const void *));

参数和 qsort 几乎一样,多了一个 key——你要找的目标值。返回的是找到的元素的指针,找不到返回 NULL。

重要:bsearch 的比较函数必须和 qsort 用的完全一致。否则结果不可预测。

int target = 23;
int *found = bsearch(&target, arr, n, sizeof(int), cmp_int);
if (found)
    printf("找到了:%d\n", *found);
else
    printf("没找到\n");

我个人习惯把 key 也封装成指针传递,这样比较函数可以统一处理。比如查找结构体时,key 可以是一个只填充了关键字段的临时结构体。

三、自定义比较函数:灵活性的灵魂

qsort 和 bsearch 的强大之处,就在于你可以通过比较函数实现任意排序逻辑。我整理了几种常见模式:

场景 比较函数写法 说明
整数升序 return ia - ib; 注意溢出风险
整数降序 return ib - ia; 交换 a、b 位置
字符串升序 return strcmp(sa, sb); 区分大小写
字符串降序 return strcmp(sb, sa); 交换参数
结构体按字段 return a->age - b->age; 先转成结构体指针
多级排序 先比字段1,相等再比字段2 用 if-else 链

多级排序示例

typedef struct {
    char name[32];
    int age;
    float score;
} Student;

int cmp_student(const void *a, const void *b) {
    const Student *sa = (const Student *)a;
    const Student *sb = (const Student *)b;

    // 先按年龄升序
    if (sa->age != sb->age)
        return sa->age - sb->age;
    // 年龄相同,按分数降序
    if (sa->score != sb->score)
        return (sa->score > sb->score) ? -1 : 1;
    // 分数也相同,按名字字典序
    return strcmp(sa->name, sb->name);
}

我在项目中遇到过需要按多个字段排序的场景,比如先按优先级、再按时间戳、最后按 ID。这种多级比较函数写起来很直观,就是一层层 if 判断。

四、排序算法选型:什么时候用什么

qsort 用的是快速排序,平均 O(n log n),最坏 O(n²)。但标准库的实现通常做了优化(比如三数取中、小数组转插入排序),所以实际表现很稳定。

不过,不是所有场景都适合用 qsort。我根据自己的经验画了张选型图:

排序算法选型决策树 数据规模多大? n < 50:插入排序 n 适中:qsort(快速排序) n 极大:归并/基数 数据是否几乎有序? 是:插入排序更优 否:qsort 足够 实际开发:90% 场景用 qsort 就够了

说说我的经验:

  • 通用场景:直接用 qsort。标准库的实现经过了大量测试,比你自己手写的快排稳定得多。
  • 小数据量(n < 50):插入排序反而更快。因为 qsort 有递归开销,插入排序的常数极小。
  • 几乎有序的数据:插入排序 O(n),qsort 反而可能退化到 O(n²)。
  • 需要稳定排序:qsort 是不稳定的。如果你需要相等元素的相对顺序不变,用归并排序。
  • 外部排序:数据量超过内存时,用归并排序的分治思想做外部排序。

注意:我曾经在一个嵌入式项目里用 qsort 排序 10 个元素的数组,结果性能分析发现排序占了 30% 的 CPU 时间。换成插入排序后,直接降到 2%。所以,别迷信 qsort,小数据量时简单算法反而更快。

五、综合实战:命令行工具的排序+搜索

最后,我写一个完整的例子。假设我们要做一个处理学生成绩的小工具:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char name[32];
    float score;
} Student;

int cmp_by_score(const void *a, const void *b) {
    const Student *sa = (const Student *)a;
    const Student *sb = (const Student *)b;
    if (sa->score < sb->score) return 1;  // 降序
    if (sa->score > sb->score) return -1;
    return 0;
}

int cmp_by_id(const void *a, const void *b) {
    const Student *sa = (const Student *)a;
    const Student *sb = (const Student *)b;
    return sa->id - sb->id;
}

int main() {
    Student students[] = {
        {3, "张三", 88.5},
        {1, "李四", 92.0},
        {2, "王五", 76.5},
        {4, "赵六", 88.5}
    };
    size_t n = sizeof(students) / sizeof(students[0]);

    // 按分数降序排序
    qsort(students, n, sizeof(Student), cmp_by_score);
    printf("按分数降序:\n");
    for (size_t i = 0; i < n; i++)
        printf("  %d %s %.1f\n", students[i].id, students[i].name, students[i].score);

    // 按 ID 排序,准备二分查找
    qsort(students, n, sizeof(Student), cmp_by_id);
    printf("\n按 ID 排序后:\n");
    for (size_t i = 0; i < n; i++)
        printf("  %d %s %.1f\n", students[i].id, students[i].name, students[i].score);

    // 二分查找 ID=2 的学生
    Student key = {2, "", 0};
    Student *found = bsearch(&key, students, n, sizeof(Student), cmp_by_id);
    if (found)
        printf("\n找到 ID=2:%s,分数 %.1f\n", found->name, found->score);
    else
        printf("\n没找到\n");

    return 0;
}

这个例子展示了两个关键点:

  1. 同一个结构体可以用不同的比较函数实现不同的排序逻辑
  2. bsearch 要求数组必须按比较函数的规则排好序

小技巧:我习惯把比较函数命名为 cmp_字段名_方向,比如 cmp_score_desccmp_id_asc。这样一看函数名就知道排序规则,不用去读实现。

好了,排序和搜索就讲到这里。qsort 和 bsearch 是 C 标准库里的黄金搭档,掌握了它们,大部分数据处理场景你都能应付。记住:比较函数是灵魂,排序前想清楚规则,查找前确认有序。做到这三点,基本不会翻车。


公众号:蓝海资料掘金营,微信deep3321