链表的反转:迭代反转、递归反转、局部反转

链表反转,说实话,是面试里考得最多的问题之一。我当年去大厂面试,第一轮手撕代码就是这道题。当时我用了递归,面试官点点头,又让我用迭代写一遍。嗯,两种方法都得会。

反转的核心思想是什么?说白了就是改变指针的方向。原来 a→b→c,变成 a←b←c。听起来简单,但写起来容易晕。我见过不少同事,写反转写到把自己绕进去。今天咱们把三种方式都捋一遍。

核心要点:反转链表不改变节点本身,只改变节点之间的指向关系。你操作的是指针,不是数据。

一、迭代反转

迭代反转是最直观的方式。你需要三个指针:prevcurrnext。为什么是三个?因为你要断开当前节点的 next 指针之前,得先保存它后面的节点,不然就找不回来了。

我刚开始学的时候,总想着两个指针就够了。结果一跑就段错误,链表断了。后来养成了习惯:改指向之前,先保存后路

// 迭代反转链表
struct ListNode* reverseList_iterative(struct ListNode* head) {
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    struct ListNode* next = NULL;
    
    while (curr != NULL) {
        // 先保存下一个节点
        next = curr->next;
        // 反转当前节点的指向
        curr->next = prev;
        // 三个指针整体后移
        prev = curr;
        curr = next;
    }
    // 循环结束时,prev 指向新头
    return prev;
}

你看这个流程:保存→反转→移动。每一步都很清晰。我曾经在嵌入式项目里用这个方式反转一个设备链表,跑了几年没出过问题。

小技巧:画图!在纸上画三个方框代表节点,用箭头标出指针变化。我每次教新人,都让他们先画图再写代码。画明白了,代码自然就写对了。

二、递归反转

递归反转,代码更短,但理解起来需要一点想象力。它的思路是:先反转后面的链表,再把当前节点接到后面

你想想看,如果我已经把 head->next 后面的部分反转好了,那现在要做什么?只需要让 head->next->next = head,再把 head->next 置空就行了。

// 递归反转链表
struct ListNode* reverseList_recursive(struct ListNode* head) {
    // 递归终止条件:空链表或只有一个节点
    if (head == NULL || head->next == NULL) {
        return head;
    }
    
    // 反转后面的链表
    struct ListNode* newHead = reverseList_recursive(head->next);
    
    // 把当前节点接到后面
    head->next->next = head;
    head->next = NULL;
    
    return newHead;
}

这里有个坑:head->next->next = head 这行,很多人写成 head->next = head,那就成环了。我曾经调试一个递归反转的 bug,花了两个小时才发现是这里写反了。嗯,血的教训。

注意:递归反转会使用栈空间,深度等于链表长度。如果链表很长(比如上万节点),小心栈溢出。嵌入式环境里栈通常比较小,我一般优先用迭代法。

三、局部反转

局部反转,就是只反转链表中的一段。比如把第 m 个节点到第 n 个节点之间的部分反转,其他部分保持不变。

这个需求我在实际项目中遇到过。有一次要处理一个数据包链表,需要把中间一段顺序颠倒一下。当时我写了个局部反转,同事看了直呼巧妙。

思路是这样的:

  1. 先找到第 m-1 个节点(反转段的前驱)
  2. 从第 m 个节点开始,用迭代法反转 n-m+1 个节点
  3. 把反转后的段重新接回原链表
// 局部反转:反转从 m 到 n 的节点(1-based)
struct ListNode* reverseBetween(struct ListNode* head, int m, int n) {
    if (head == NULL || m == n) {
        return head;
    }
    
    // 加一个虚拟头节点,方便处理 m=1 的情况
    struct ListNode dummy;
    dummy.next = head;
    struct ListNode* prev = &dummy;
    
    // 1. 找到第 m-1 个节点
    for (int i = 1; i < m; i++) {
        prev = prev->next;
    }
    
    // 2. 开始反转
    struct ListNode* curr = prev->next;
    struct ListNode* next = NULL;
    struct ListNode* tail = curr;  // 反转段的尾巴,最后要接上
    
    for (int i = m; i <= n; i++) {
        next = curr->next;
        curr->next = prev->next;
        prev->next = curr;
        curr = next;
    }
    
    // 3. 把反转后的尾巴接上剩余部分
    tail->next = curr;
    
    return dummy.next;
}

这段代码里,我用了虚拟头节点 dummy。为什么?因为如果 m=1,反转段包含头节点,没有前驱节点,处理起来很麻烦。加个虚拟头,一切就统一了。这个技巧我在很多链表操作里都用过。

关键点:局部反转的难点在于边界处理。m=1 时头节点会变,n=链表长度时尾巴要接 NULL。画个图,把每个指针的最终位置标清楚,再动手写。

三种方式对比

方法 时间复杂度 空间复杂度 适用场景
迭代反转 O(n) O(1) 通用,推荐日常使用
递归反转 O(n) O(n)(栈空间) 链表较短,代码简洁优先
局部反转 O(n) O(1) 需要反转部分节点

我个人习惯,能迭代就不递归。不是递归不好,而是嵌入式环境里栈资源太宝贵。你想想看,一个链表几千个节点,递归下去栈就爆了。迭代法三个指针走天下,多稳。

知识体系图

下面这张图展示了链表反转的三种方式及其关系,帮你理清思路:

链表反转知识体系 链表反转 迭代反转 递归反转 局部反转 特性 • 三个指针:prev/curr/next • 空间 O(1),无栈开销 • 适合长链表、嵌入式环境 特性 • 代码简洁,逻辑清晰 • 空间 O(n),栈深度=长度 • 适合短链表、教学演示 特性 • 反转指定区间 [m, n] • 需处理边界 m=1 情况 • 常用虚拟头节点技巧

我的建议:先掌握迭代反转,这是基本功。递归反转用来理解递归思想,面试时可能会考。局部反转是实际工程中常用的,处理数据包、任务队列时经常用到。

好了,链表反转就讲到这里。三种方式各有千秋,你根据实际场景选就行。记住一点:画图永远比硬想快。我到现在写链表操作,第一件事还是拿笔在纸上画。


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