指针与排序算法:冒泡排序的指针实现、快速排序的指针实现、回调函数实现通用排序

排序算法,是每个嵌入式工程师都绕不开的基本功。我记得刚入行那会儿,面试官让我手写一个冒泡排序,我刷刷刷写完了,他看了一眼说:“能不能用指针改一下?”我当时一愣——用数组下标不是好好的吗?后来我才明白,指针操作在底层数据交换、内存受限的场景下,优势非常明显。

今天这一章,我们就来聊聊怎么用指针实现两种经典排序:冒泡和快速。最后再上一个“大招”——用回调函数把排序算法做成通用的,想排什么类型就排什么类型。

一、冒泡排序的指针实现

冒泡排序的原理很简单:相邻元素两两比较,大的往后“冒”。用数组下标写,大家都会。但用指针写,能让你更深刻地理解内存操作。

我个人习惯在嵌入式项目里用指针操作数组,因为很多底层寄存器、缓冲区都是地址连续的。你想想看,用指针自增去遍历,比用下标索引更贴近硬件思维。

核心思路: 用两个指针分别指向待比较的元素,通过解引用取值比较,然后交换。
#include <stdio.h>

void bubble_sort_ptr(int *arr, int n) {
    int *p, *q, tmp;
    for (int i = 0; i < n - 1; i++) {
        // p 指向数组起始,q 指向下一个元素
        p = arr;
        q = arr + 1;
        for (int j = 0; j < n - 1 - i; j++) {
            if (*p > *q) {
                tmp = *p;
                *p = *q;
                *q = tmp;
            }
            p++;
            q++;
        }
    }
}

int main() {
    int a[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(a) / sizeof(a[0]);
    bubble_sort_ptr(a, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

你看,这里 pq 就是两个“游标指针”,每次比较完就一起后移。我在项目中调试过一段类似的代码,当时有个坑:内层循环的边界条件写错了,导致指针越界。嗯,这里要注意——指针移动一定要保证不超出数组范围。

小技巧: 用指针实现冒泡排序时,可以定义一个 int *end = arr + n - 1 - i 作为终止条件,这样代码可读性更好。

二、快速排序的指针实现

快速排序是“分治思想”的典型代表。用指针实现时,核心是分区函数——它需要两个指针从两端向中间扫描。

我曾经在做一个数据采集系统时,需要对传感器数据实时排序。冒泡太慢,就换成了快排。当时用指针写分区函数,调试了一下午才跑通。说白了,指针的移动逻辑一旦理清楚,代码会非常简洁高效。

#include <stdio.h>

void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int *partition(int *low, int *high) {
    int pivot = *high;   // 选最后一个元素作为基准
    int *i = low - 1;    // i 指向小于基准的区域的最后一个位置

    for (int *j = low; j < high; j++) {
        if (*j < pivot) {
            i++;
            swap(i, j);
        }
    }
    swap(i + 1, high);
    return i + 1;
}

void quick_sort_ptr(int *low, int *high) {
    if (low < high) {
        int *pi = partition(low, high);
        quick_sort_ptr(low, pi - 1);
        quick_sort_ptr(pi + 1, high);
    }
}

int main() {
    int a[] = {10, 7, 8, 9, 1, 5};
    int n = sizeof(a) / sizeof(a[0]);
    quick_sort_ptr(a, a + n - 1);
    for (int i = 0; i < n; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

这里 partition 返回的是基准元素的指针。你注意看,i 初始化为 low - 1,这是为了在第一次比较时能正确工作。我刚开始写的时候,把 i 初始化为 low,结果分区逻辑全乱了。

避坑指南: 指针做减法时,结果是以“元素个数”为单位的,不是字节数。比如 high - low 得到的是两个指针之间相差的元素个数,这在递归终止条件判断中非常有用。

三、回调函数实现通用排序

上面两个排序都只能排 int 类型。如果我想排 floatdouble,甚至结构体呢?难道每种类型都写一遍?

当然不用。C 标准库里的 qsort 就是最好的例子——它通过回调函数来比较任意类型的数据。我们完全可以模仿它,写出自己的通用排序函数。

说白了,就是把“比较”这个动作抽象出来,交给调用者决定。你想想看,这样代码的复用性一下子就上来了。

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

// 通用交换函数:按字节交换
void swap_bytes(void *a, void *b, size_t size) {
    char tmp[size];
    memcpy(tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, tmp, size);
}

// 通用冒泡排序
void generic_bubble_sort(void *base, size_t n, size_t size,
                         int (*cmp)(const void *, const void *)) {
    char *arr = (char *)base;
    for (size_t i = 0; i < n - 1; i++) {
        for (size_t j = 0; j < n - 1 - i; j++) {
            void *p = arr + j * size;
            void *q = arr + (j + 1) * size;
            if (cmp(p, q) > 0) {
                swap_bytes(p, q, size);
            }
        }
    }
}

// 比较函数示例:比较两个整数
int cmp_int(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int main() {
    int a[] = {34, 12, 56, 78, 23, 9};
    int n = sizeof(a) / sizeof(a[0]);
    generic_bubble_sort(a, n, sizeof(int), cmp_int);
    for (int i = 0; i < n; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

这里的关键是 void * 指针和 size 参数。我们把数组当成连续的字节块,通过 char * 指针按字节移动。比较函数由调用者提供,交换函数按字节复制。

核心思想: 回调函数让排序算法与数据类型解耦。你只需要提供一个比较函数,剩下的排序逻辑完全通用。

我在一个项目里用这个模式写过排序模块,支持 intfloat、结构体(按某个字段排序)。调用方只需要注册自己的比较函数,代码量减少了一半以上。

四、知识体系总览

下面这张图帮你理清本章的核心逻辑:指针如何贯穿排序的各个环节。

指针与排序算法知识体系 冒泡排序(指针实现) • 双游标指针遍历 • 指针自增与边界控制 • 解引用比较与交换 快速排序(指针实现) • 双指针分区扫描 • 基准元素指针返回 • 递归调用与指针运算 通用排序(回调函数) • void* 指针与字节操作 • 回调比较函数接口 • 按字节交换(memcpy) 核心收获 1. 指针让排序操作更贴近内存本质,效率更高 2. 回调函数实现“算法与数据分离”,代码复用性极强 3. 掌握指针运算和字节操作,是嵌入式开发的必备技能

从这张图可以看得很清楚:冒泡和快排是“术”,指针操作是“器”,回调函数是“道”。三者结合,你就能写出既高效又通用的排序代码。

我的建议: 初学者先吃透冒泡排序的指针实现,再挑战快排。最后再理解回调函数的通用思想。一步一个脚印,别贪快。

好了,这一章的内容就到这里。指针与排序的结合,说白了就是让你从“会用数组”进阶到“理解内存”。下一章我们会继续深入指针的高级应用,敬请期待。


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