9、链表与递归:递归遍历链表、递归反转链表、递归的优缺点

递归,这个词我当年学的时候,觉得它很玄乎。说白了,就是函数自己调用自己。你想想看,一个函数还没执行完,又调了一个自己,这栈帧一层层压下去,确实容易让人头晕。

但链表这东西,天生就适合递归。为啥?因为链表本身就是递归定义的——一个节点后面跟着另一个节点,直到 NULL。你顺着这个思路去想,递归遍历和递归反转,其实就顺理成章了。

9.1 递归遍历链表

先看最简单的:递归遍历。我记得刚入行那会儿,有个同事写链表遍历非要用递归,结果线上出了栈溢出。嗯,这里要注意,递归深度太深确实有风险。但理解递归遍历,是理解递归反转的基础。

递归遍历的核心思路:

  • 如果当前节点是 NULL,就返回(递归终止条件)
  • 否则,先处理当前节点,再递归处理下一个节点

代码长这样:

// 递归遍历链表(正序打印)
void traverse_recursive(Node* head) {
    if (head == NULL) {
        return;
    }
    printf("%d ", head->data);   // 先访问当前节点
    traverse_recursive(head->next); // 再递归下一个
}

如果你想逆序打印呢?很简单,把 printf 放到递归调用后面:

// 递归遍历链表(逆序打印)
void traverse_reverse_recursive(Node* head) {
    if (head == NULL) {
        return;
    }
    traverse_reverse_recursive(head->next); // 先递归到底
    printf("%d ", head->data);   // 回溯时打印
}

我在项目中遇到过一种场景:需要从尾到头输出日志链表。用迭代法得先反转再遍历,或者用栈。递归一行代码就搞定了——当然,前提是链表不太长。

小技巧:递归遍历的「先处理再递归」就是前序,「先递归再处理」就是后序。这个思路在树结构里同样适用。

9.2 递归反转链表

递归反转链表,是面试高频题。我第一次看到这个解法时,觉得这代码也太优雅了——但理解起来确实需要点功夫。

核心思想:

  • 递归到最后一个节点,它就是新头
  • 回溯时,把当前节点的 next 的 next 指向自己
  • 再把当前节点的 next 置为 NULL

直接看代码:

// 递归反转链表,返回新头
Node* reverse_recursive(Node* head) {
    // 终止条件:空链表或只有一个节点
    if (head == NULL || head->next == NULL) {
        return head;
    }
    
    // 递归反转后面的链表
    Node* new_head = reverse_recursive(head->next);
    
    // 关键步骤:让下一个节点指向自己
    head->next->next = head;
    head->next = NULL;
    
    return new_head;
}

我画个图帮你理解这个过程:

递归反转链表过程示意图 原始链表: 1 2 3 NULL 递归到底: 1 2 3 NULL 回溯反转: 1 2 反转指向 3 NULL 最终结果: 3 2 1 NULL new_head 最终指向 3 每次回溯修改 next 指向

这段代码的精髓在于:head->next->next = head。我第一次看这行时,愣了半天。说白了,就是让下一个节点反过来指向自己。你想想看,原来 1→2→3,递归到 3 返回后,在 2 这一层,head 是 2,head->next 是 3,那么 head->next->next = head 就是 3→2。

注意:递归反转后,原来的头节点的 next 必须置为 NULL,否则会形成环。我见过有人忘了这步,结果遍历时死循环了。

9.3 递归的优缺点

递归不是银弹。我用个表格给你对比一下:

对比项 递归 迭代
代码简洁性 非常简洁,逻辑清晰 相对冗长,需要手动维护指针
空间复杂度 O(n),需要栈空间 O(1),只使用几个指针
性能 函数调用有开销,可能较慢 通常更快
可读性 对熟悉递归的人很好 对所有人友好
栈溢出风险 链表太长时可能溢出 无此风险
调试难度 较难,调用栈深 容易,单步跟踪

我个人习惯是:链表长度可控(比如几百个节点以内)时,用递归写反转和遍历,代码漂亮。但如果是生产环境,链表可能很长,我建议用迭代。我曾经在一个嵌入式项目里,链表有上万节点,递归直接栈溢出,改成迭代后稳如老狗。

避坑指南:我曾经在 RTOS 的任务栈里用递归反转链表,结果栈空间只有 2KB,链表才 500 个节点就崩了。从那以后,我在嵌入式环境里对递归格外谨慎——栈空间是稀缺资源。

9.4 什么时候用递归?

给你几个判断标准:

  • 链表长度已知且较小(比如 < 1000)—— 可以用
  • 代码可读性优先(比如教学、原型)—— 推荐用
  • 性能敏感(比如高频调用)—— 别用
  • 栈空间紧张(比如单片机)—— 千万别用
  • 需要逆序处理(比如从尾到头打印)—— 递归很自然

说白了,递归是一种思维工具。它让你用更少的代码表达更复杂的逻辑。但工具要用对地方,别拿着锤子看什么都像钉子。

我的建议:先理解递归的思路,面试时能写出来。但实际项目中,除非你确定链表很短,否则优先用迭代。安全第一。

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