指针与算法:排序、查找与字符串匹配

指针和算法,这两个东西放在一起,很多人第一反应是「头大」。其实没那么复杂。我做了十几年嵌入式开发,指针用得最多的场景,恰恰就是算法实现。今天咱们就聊聊排序、查找、字符串匹配这些经典算法里,指针到底扮演了什么角色。

排序算法中的指针交换

先说说排序。你写冒泡排序、快速排序,核心操作是什么?交换元素。对吧?

交换两个整型变量,你可能会这么写:

int temp = a;
a = b;
b = temp;

但如果交换的是结构体呢?比如一个包含 1KB 数据的结构体:

struct big_data {
    char buffer[1024];
    int id;
    double value;
};

直接交换两个结构体变量,C 语言会做一次内存拷贝,1024 字节搬来搬去。效率?你自己想想看。

这时候指针就派上用场了。

我们不交换结构体本身,只交换指向它们的指针:

struct big_data *pa, *pb, *ptemp;
ptemp = pa;
pa = pb;
pb = ptemp;

一次指针赋值,4 个字节(32位系统)或 8 个字节(64位系统)搞定。比搬 1KB 数据快了两个数量级。

核心原则:排序时,如果元素体积大,交换指针而非交换数据。这是工程优化的基本思路。

我在项目中遇到过一个问题:一个嵌入式设备上跑排序算法,数据量不大,但每个元素是一个 2KB 的日志结构体。一开始直接交换结构体,排序花了 3 秒多。改成指针交换后,降到 0.2 秒。嗯,差距就这么大。

查找算法中的指针优化

二分查找,大家都会写。但用指针实现,有个好处——避免数组越界

看这段代码:

int* binary_search(int* arr, int size, int target) {
    int* left = arr;
    int* right = arr + size - 1;
    
    while (left <= right) {
        int* mid = left + (right - left) / 2;
        if (*mid == target) return mid;
        else if (*mid < target) left = mid + 1;
        else right = mid - 1;
    }
    return NULL;
}

注意看 mid = left + (right - left) / 2。为什么不用 (left + right) / 2

因为指针相加是未定义行为。你想想看,两个指针相加,结果指向哪里?没人知道。但指针相减,得到的是两个地址之间的元素个数,这是合法的。

避坑指南:我曾经在代码里写过 (left + right) / 2,编译器没报错,但运行结果时对时错。查了两天才发现是指针相加的问题。从此以后,我写二分查找只用指针相减的方式。

另外,用指针做查找还有一个好处:返回值天然就是地址。你找到元素了,直接返回指针,调用方拿到指针就能读写这个元素,不需要再通过下标去换算地址。

字符串匹配算法(KMP)的指针实现

KMP 算法,很多人觉得难。其实它的核心就一句话:匹配失败时,利用已匹配部分的信息,跳过不必要的比较

用指针实现 KMP,比用下标更直观。因为字符串本身就是字符数组,指针操作天然适合。

先看 next 数组的指针实现:

void get_next(const char* pattern, int* next) {
    int i = 0;
    int j = -1;
    next[0] = -1;
    
    while (pattern[i] != '\0') {
        if (j == -1 || pattern[i] == pattern[j]) {
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

再看匹配过程:

const char* kmp_search(const char* text, const char* pattern) {
    int next[256];
    get_next(pattern, next);
    
    int i = 0, j = 0;
    while (text[i] != '\0' && pattern[j] != '\0') {
        if (j == -1 || text[i] == pattern[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    
    if (pattern[j] == '\0') {
        return &text[i - j];  // 返回匹配位置的指针
    }
    return NULL;
}

你看,返回的是 &text[i - j],一个指向匹配起始位置的指针。调用方拿到这个指针,可以直接从这个位置开始处理,不需要再算下标。

个人习惯:我写字符串处理函数,返回值一律用指针。这样接口更统一,调用方用起来也顺手。比如 strstr() 标准库就是这么设计的。

递归与指针

递归和指针结合,最常见的场景就是链表和树的操作。比如反转链表:

struct node* reverse(struct node* head) {
    if (head == NULL || head->next == NULL) {
        return head;
    }
    struct node* new_head = reverse(head->next);
    head->next->next = head;
    head->next = NULL;
    return new_head;
}

这段代码,说白了就是:先递归到链表末尾,然后从后往前,逐个反转指针方向。

递归的深度取决于链表长度。如果链表有 10000 个节点,递归深度就是 10000。栈空间够吗?

我建议你在嵌入式系统里慎用递归。因为嵌入式设备的栈通常很小,可能只有几 KB。递归深度稍微大一点,栈就溢出了。

我曾经踩过的坑:在一个 RTOS 项目里,用递归遍历二叉树。树深度只有 50 层,但每个任务分配的栈只有 512 字节。结果呢?递归到第 30 层就栈溢出了,系统直接 HardFault。后来改成迭代 + 指针栈,问题解决。

递归和指针还有一个经典组合:函数指针实现回调。比如快速排序的 qsort(),它的比较函数就是通过函数指针传入的。这种设计模式在算法库中非常常见。

知识体系总览

下面这张图,把指针在算法中的几个核心应用场景串起来了:

指针与算法 排序算法 指针交换代替数据交换 查找算法 指针运算避免越界 KMP匹配 指针返回匹配位置 递归与指针 链表/树操作 + 函数指针 核心:指针是地址,操作地址比操作数据快

这张图展示了指针在算法中的四个主要应用方向。每个方向都有它的独特价值,但核心思想是一致的:指针操作的是地址,地址操作比数据操作更轻量、更灵活

小结

指针和算法结合,说白了就是两件事:

  • 用指针代替数据——排序时交换指针,查找时返回指针,减少数据搬运
  • 用指针控制流程——递归遍历链表树,函数指针实现回调,让代码更灵活

我在实际项目中,80% 的算法优化都跟指针有关。不是算法本身有多复杂,而是数据怎么搬、地址怎么算,这些细节决定了最终性能。

嗯,这一章就到这里。记住一句话:指针不是洪水猛兽,它是 C 语言给你的一把手术刀——用好了,精准高效;用不好,割到自己。


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