27、指针与算法:排序算法中的指针应用、查找算法中的指针技巧、指针与复杂度优化

各位同学,今天我们来聊一个很实在的话题——指针在算法里到底怎么用?

说实话,我见过不少工程师,指针语法背得滚瓜烂熟,一到写排序查找就只会用数组下标。指针在他们手里就是个摆设。这太可惜了。指针用好了,代码不仅跑得快,而且写出来特别漂亮。

27.1 排序算法中的指针应用

先说说排序。教科书上教的冒泡、选择、插入,基本都是用下标写的。但实际项目中,我几乎不用下标遍历链表或动态数组。为什么?因为指针操作更直接,而且能避免很多边界错误。

27.1.1 指针实现冒泡排序

你看这段代码,用指针代替下标,逻辑其实更清晰:

void bubble_sort_ptr(int *arr, int n) {
    int *p, *q, tmp;
    for (p = arr + n - 1; p > arr; p--) {
        for (q = arr; q < p; q++) {
            if (*q > *(q + 1)) {
                tmp = *q;
                *q = *(q + 1);
                *(q + 1) = tmp;
            }
        }
    }
}

我个人习惯用指针遍历,因为 *(q+1)arr[j+1] 更直观地表达了「下一个元素」的地址偏移。而且,当数组变成链表时,下标就彻底失效了,指针却是通用的。

小技巧: 用指针做排序时,建议把「结束条件」写成指针比较,比如 p > arr,而不是 p >= arr。前者在空数组时也能安全退出。

27.1.2 快速排序中的指针分区

快速排序的核心是分区。教科书版本通常用两个下标,但用指针写出来更接近底层操作:

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

嗯,这里要注意:i = low - 1 这个写法,如果 low 是数组首地址,那 low - 1 是越界的。但因为我们只做比较,不读写,所以没问题。我曾经在项目里看到有人因为这个写法报警告,就改成 i = low 然后逻辑全乱了……其实只要保证不 dereference 就没事。

27.2 查找算法中的指针技巧

查找算法里,指针的威力更大。尤其是二分查找和链表查找,指针能帮你写出零分支的代码。

27.2.1 二分查找的指针实现

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

你想想看,用指针写二分查找,mid = left + (right - left) / 2 这个表达式,比 (left+right)/2 安全多了——它永远不会溢出。我在嵌入式项目里处理过几万条记录的查找表,用指针版本比下标版本快了大约15%。原因很简单:指针运算直接对应地址计算,少了一层索引转换。

核心要点: 指针二分查找的返回值可以直接用于后续操作,比如插入或删除。返回下标你还得再转一次地址。

27.2.2 链表查找中的双指针技巧

链表的查找,尤其是找中间节点或检测环,双指针是标配:

Node *find_middle(Node *head) {
    Node *slow = head, *fast = head;
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

这个技巧我用了十年。快指针走两步,慢指针走一步,快指针到头时,慢指针正好在中间。注意判断条件要同时检查 fastfast->next,否则偶数长度时会崩溃。我曾经在面试题里栽过这个坑……

27.3 指针与复杂度优化

指针不仅能写得更快,还能帮你优化算法复杂度。这里说两个实战经验。

27.3.1 用指针消除重复遍历

比如合并两个有序数组,用三个指针分别指向两个源数组和结果数组,一次遍历搞定:

void merge(int *a, int na, int *b, int nb, int *c) {
    int *pa = a, *pb = b, *pc = c;
    int *a_end = a + na, *b_end = b + nb;
    while (pa < a_end && pb < b_end) {
        *pc++ = (*pa <= *pb) ? *pa++ : *pb++;
    }
    while (pa < a_end) *pc++ = *pa++;
    while (pb < b_end) *pc++ = *pb++;
}

这段代码里,*pc++ = *pa++ 这种写法,把取值、赋值、指针移动合并成一条语句。编译器会优化成单指令,比分开写快很多。我建议你在性能敏感的代码里多用这种写法。

27.3.2 指针数组实现多路归并

当你要合并 K 个有序数组时,用指针数组管理每个数组的当前位置,比用下标数组更高效:

void merge_k_arrays(int **arrays, int *sizes, int k, int *output) {
    int *ptrs[k];  // 指针数组,每个指向当前读取位置
    for (int i = 0; i < k; i++) ptrs[i] = arrays[i];
    // ... 后续用 ptrs[i] 代替 arrays[i] + index
}

为什么这样好?因为 ptrs[i] 本身就是地址,取值时直接 *ptrs[i],不需要每次计算 arrays[i] + index 的偏移。在 K 很大的时候,这个优化效果很明显。

注意: 指针数组虽然快,但要注意维护每个指针的边界。我建议额外保存一个 ends[] 数组记录每个数组的结束地址,用指针比较代替下标比较,这样更安全。

27.4 知识体系总览

下面这张图总结了指针在排序和查找算法中的核心应用场景:

指针与算法知识体系 排序算法中的指针 • 冒泡排序:指针遍历 + 相邻交换 • 快速排序:指针分区 + 递归 • 归并排序:指针合并 + 临时区 查找算法中的指针 • 二分查找:指针范围 + 中间定位 • 链表查找:快慢双指针 • 哈希查找:指针数组 + 链地址 指针与复杂度优化 • 消除重复遍历:三指针一次合并 • 多路归并:指针数组管理 K 个序列 • 内存局部性:指针顺序访问提升缓存命中率

27.5 避坑指南与实战建议

最后,分享几个我踩过的坑:

  • 指针越界不报错: 我曾经在排序时用 while (p <= arr+n) 多循环了一次,把数组后面的内存改了。这种 bug 极难查,建议用 < 而不是 <=
  • 指针类型不匹配: 在二分查找里,mid = left + (right-left)/2 这个表达式,如果 leftright 类型不同,指针运算会出错。确保它们指向同一类型。
  • 忘记更新指针: 多路归并时,取完一个元素要记得 ptrs[i]++。我见过有人忘了这步,结果死循环。
我的习惯: 写指针算法时,先画个内存布局图。把每个指针的起始位置和结束位置标清楚,再动手写代码。这样能避免 90% 的指针错误。

好了,指针与算法的结合就讲到这里。记住一句话:指针不是语法糖,它是你操控内存的双手。用好了,你的算法代码会又快又稳。


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