反转链表完全指南:辅助容器、三指针、头插法

反转单链表 是面试与教材里极高频的一题:给定头结点,把 1 → 2 → 3 → nullptr 变成 3 → 2 → 1 → nullptr

本文在统一约定下,讲清三种典型思路------vector(或栈)先存再连三指针原地迭代头插法(推荐 dummy head)重建 ------各自的时间空间、适用场景与实现细节。

文末附带递归写法(本质是「隐式栈」)与常见坑。


0. 约定:结点与「谁拥有 next」

LeetCode 风格常用裸指针:

cpp 复制代码
struct ListNode {
    int val;
    ListNode* next;
    explicit ListNode(int v = 0, ListNode* n = nullptr) : val(v), next(n) {}
};

工程里更常见 std::unique_ptr<ListNode> 管整条链(见仓库 LinkedList.hpp)。反转的本质操作都是「改 next 指向」 ;下面为突出算法,代码以裸指针为主,最后一节说明 unique_ptr 时要注意的移动语义。

目标形态(整篇共用同一例子)

text 复制代码
反转前:  head ──▶ [1] ──▶ [2] ──▶ [3] ──▶ nullptr

反转后:  head ──▶ [3] ──▶ [2] ──▶ [1] ──▶ nullptr
           ▲
           └── 新的头结点(返回值)

1. 方法一:用 vector(或栈)辅助

1.1 思路

  1. 从表头沿 next 走一遍,把每个结点的指针 (或值)依次 push_backvector
  2. 再从尾到头(或正序遍历 vector)重新串 next:让 v[i]->next = v[i-1],最后 v[0]->next = nullptr,新头为 v.back()

也可以只存 valvector<int>,第二遍用同一批结点按逆序重赋值------若结点结构只有 val 且允许改值 ,与存指针等价;一般面试里ListNode* 更直接。

1.1.1 图解:先「排成一排」,再按逆序接线

第一步:沿链走一遍,指针依次进 vector

text 复制代码
链表:     [1] ──▶ [2] ──▶ [3] ──▶ nullptr

vector:   +-----+-----+-----+
           | *1  | *2  | *3  |     下标:  0    1    2
           +-----+-----+-----+

第二步:v[i]->next = v[i-1],最后 v[0]->next = nullptr,返回 v.back()

text 复制代码
v[1]->next = v[0]   即  [2] ──▶ [1]
v[2]->next = v[1]   即  [3] ──▶ [2]
v[0]->next = nullptr  即  [1] ──▶ nullptr

结果:     [3] ──▶ [2] ──▶ [1] ──▶ nullptr
            ▲
            └── 新头 = v.back()
cpp 复制代码
#include <vector>

ListNode* reverseWithVector(ListNode* head) {
    if (!head) return nullptr;
    std::vector<ListNode*> nodes;
    for (ListNode* p = head; p; p = p->next)
        nodes.push_back(p);
    for (size_t i = 1; i < nodes.size(); ++i)
        nodes[i]->next = nodes[i - 1];
    nodes[0]->next = nullptr;
    return nodes.back();
}

1.3 复杂度与评价

项目 结果
时间 O(n),遍历两遍(收集 + 重连)
额外空间 O(n),存放 n 个指针
优点 思路直白、不易写错 next 顺序;可顺便做「第 k 个翻转」等需要随机访问下标的变形
缺点 额外数组;超长链可能受 vector 扩容影响(常数因子)

若题目要求 O(1) 额外空间,此法不满足,需换下面两种。

用栈 :把 vector 换成 stack<ListNode*>,先一路 push,再一路 pop 重连,逻辑与 vector 逆序等价,空间仍是 O(n),只是写法不同。

text 复制代码
push 顺序:  栈底 ... [1][2][3] 栈顶
pop 接线:  先拿到 3,再 2,再 1  ------ 天然逆序

2. 方法二:三指针(原地迭代,面试标配)

2.1 思路

维护三个指针(或两个指针 + 一个临时):

  • prev:已反转部分的头(初始 nullptr
  • cur:当前要处理的结点
  • nxt(可选):保存 cur->next,避免改链后丢失后继

每一步:先把 cur->next 指回 prev,再把三个指针整体右移一格。本质是沿着链表走一遍,每次把一条边「反向」。

2.1.1 图解:三指针走一遍(每轮改一条边)

初始

text 复制代码
  prev          cur                    nxt
   │             │                      │
   ▼             ▼                      ▼
 nullptr       [1] ─────────────────▶ [2] ──▶ [3] ──▶ nullptr

第 1 轮nxt = cur->nextcur->next = prev,然后 prev←curcur←nxt

text 复制代码
 nullptr ◀── [1]        [2] ──▶ [3] ──▶ nullptr
   ▲          ▲
 prev       cur(下一轮从这里继续)

第 2 轮之后同理 ,直到 cur 为空:

text 复制代码
 nullptr ◀── [1] ◀── [2] ◀── [3]
                              ▲
                            prev = 新头

上面「一行里画三个指针」的紧凑写法:

text 复制代码
  nullptr  ◀──  1     2  ──▶  3  ──▶  nullptr
   prev          cur   nxt

循环不变式:进入每轮迭代前,prev 指向的链表段已全部反转cur 指向第一个尚未反转的结点。

2.2 代码

cpp 复制代码
ListNode* reverseThreePointers(ListNode* head) {
    ListNode* prev = nullptr;
    ListNode* cur = head;
    while (cur) {
        ListNode* nxt = cur->next;  // 先留住后继,否则下面一行会丢
        cur->next = prev;
        prev = cur;
        cur = nxt;
    }
    return prev;  // 结束时 cur 为 nullptr,prev 是新头
}

2.3 复杂度与评价

项目 结果
时间 O(n),单趟
额外空间 O(1),只有几个指针
优点 空间最优;代码短,面试最常写
缺点 指针顺序写反容易断链;必须先保存 nxt 再改 cur->next

「三指针」有时也指 prev, cur, nxt 显式三个变量;也有教材写 p, q, r 交替前移,本质相同。


3. 方法三:头插法(dummy head 推荐)

3.1 思路

仍是从旧链逐个摘下 结点,但每次把摘下的结点插到「反转结果链的表头前 」。用 dummy 哨兵结点 挂在结果链前面,统一用 dummy.next 表示当前反转链的头

  1. ListNode dummy(0);val 无意义,仅占位;next 默认为 nullptr
  2. 循环:从旧链取 node = headhead = head->next
  3. 头插到 dummy 后面node->next = dummy.next; dummy.next = node;

这样每一轮都是同一套两句指针操作,不必单独维护 new_head 变量 ;空表时直接返回 dummy.next(为 nullptr),也无需分支。

3.1.1 图解:dummy 后永远是「当前反转链」的头

初始 :旧链 headdummy.next 为空

text 复制代码
  dummy                head
    │                    │
    ▼                    ▼
  [ D ] ──▶ nullptr     [1] ──▶ [2] ──▶ [3] ──▶ nullptr

摘下 1 并头插到 dummynode->next = dummy.next; dummy.next = node;

text 复制代码
  dummy
    │
    ▼
  [ D ] ──▶ [1] ──▶ nullptr          head ──▶ [2] ──▶ [3] ──▶ nullptr
            ▲
            └── dummy.next

再摘下 2

text 复制代码
  [ D ] ──▶ [2] ──▶ [1] ──▶ nullptr          head ──▶ [3] ──▶ nullptr

再摘下 3,返回 dummy.next

text 复制代码
  [ D ] ──▶ [3] ──▶ [2] ──▶ [1] ──▶ nullptr

返回值:dummy.next ──▶ [3] ──▶ ...(哨兵 D 不进入业务链)

若题目禁止在堆上 new 额外结点,可用栈上的局部变量 ListNode dummy(0);,只多占一个结点大小的栈空间,不增加堆分配。

3.2 代码(dummy head)

cpp 复制代码
ListNode* reverseHeadInsertDummy(ListNode* head) {
    ListNode dummy(0);       // 栈上哨兵;next 默认为 nullptr
    while (head) {
        ListNode* node = head;
        head = head->next;           // 旧链先走一步
        node->next = dummy.next;     // 接到「当前反转链」表头之前
        dummy.next = node;           // dummy 后始终是当前反转链的头
    }
    return dummy.next;
}

3.3 与「裸 new_head 指针」版等价

不用 dummy 时,上面两步对应 node->next = new_head; new_head = node;dummy.next 扮演的角色就是 new_head ,只是插入位置写成「插在 dummy 与第一个反转结点之间」,和「链头前插入」在链表语义上相同。

3.4 与三指针的关系

两者都是原地、单趟、O(1) 空间(dummy 只占常数栈帧)。差别在心智模型:

  • 三指针 :在原链上 改边方向,prev 拖着已反转段走。
  • 头插法 + dummy :结果链挂在 dummy 后面,每轮往 dummy一个结点;写「反转中间一段」等题时,dummy 模式更容易推广。

写对了复杂度完全一样,面试里任选一种背熟即可;头插法更推荐带 dummy,代码形态统一、少一个手写变量名。


4. 递归:隐式栈,空间 O(n)

cpp 复制代码
ListNode* reverseRecursive(ListNode* head) {
    if (!head || !head->next) return head;
    ListNode* new_head = reverseRecursive(head->next);
    head->next->next = head;
    head->next = nullptr;
    return new_head;
}

递归到尾结点后,自底向上把每条边反向。调用栈深度为 O(n) ,故严格说额外空间是 O(n),不满足常数空间要求,但代码短、适合理解「反转 = 后序处理子问题」。

4.1 图解:每一层只「翻一条边」

1 → 2 → 3 为例:递归先处理 2 → 3,子问题返回后,右侧已是 3 → 2 → nullptr ,而 1 → 2 这条旧边还在1next 尚未改)。此时内存里相当于「2 同时被 13 指着」,这是中间态,只在当前栈帧里存在一瞬:

text 复制代码
  子问题刚返回、尚未改当前层时:

        [3] ──▶ [2] ──▶ nullptr
                ▲
                │
        [1] ────┘        ([1] 的 next 仍指向 [2])

当前层执行 head->next->next = headhead->next = nullptr 后,得到线性表:

text 复制代码
        [3] ──▶ [2] ──▶ [1] ──▶ nullptr

每一层各处理一条「回头边」,栈帧全部弹完即整条链反转完毕。

5. 对照小结

方法 时间 额外空间 特点
vector / 栈 O(n) O(n) 最直观;可扩展需要下标的题
三指针 O(n) O(1) 面试默认答案
头插法(dummy) O(n) O(1) 与三指针等价类;dummy 统一插入形态,易推广到「反转区间」
递归 O(n) O(n) 理解用;深度大时可能栈溢出

6. 常见坑

  1. cur->next 前没保存 cur->next :三指针里若先写 cur->next = prev 再取后继,后继已丢,只能得到半截链或死循环。
  2. 返回错指针 :迭代结束时新头是 prev(三指针)或 dummy.next(头插法) ,不是原来的 head
  3. 空表 / 单结点 :上述写法一般已覆盖;递归注意 !head->next 的边界。
  4. unique_ptr 版本 :不能简单「交换裸指针」而不移动所有权;通常要改写为「按 unique_ptr 移动子树」或仍用裸指针 Node* 做遍历游标、由类对象持有 headunique_ptr(实现细节见 LinkedList.hpp)。

7. 练习与扩展

  • LeetCode 206:反转链表(基础)
  • LeetCode 92:反转链表 II(反转中间一段:vector 下标或三指针分段)
  • LeetCode 24:两两交换(头插 / 三指针变形)

8. 一句话总结

  • vector / 栈 :先收集再逆序连接,O(n) 空间,最好想。
  • 三指针 :原地改向,O(1) 空间,最常考。
  • 头插法 :推荐 dummy.next 作反转链头 ,摘旧结点插到 dummy 后,O(1) 空间,与三指针二选一熟练即可。

记住三指针里的一句口诀:先存后继,再改指向,最后整体右移。

头插法带 dummy 时:先摘旧头、再 node->next = dummy.nextdummy.next = node ,返回 dummy.next

相关推荐
我不是懒洋洋1 小时前
从零实现一个分布式配置中心:服务发现与热更新
c++
省四收割者1 小时前
从硬件中断到分布式协程:全景解构高并发机制与 C / Golang 的巅峰对决
c++·分布式·嵌入式硬件·golang
Cx330❀1 小时前
【Linux网络】从零定制应用层协议:黏包问题、全双工缓冲区与 Jsoncpp 序列化深度解析
linux·运维·服务器·开发语言·网络·c++·人工智能
程序猿零零漆1 小时前
Python进阶之路:正则表达式、高级语法与核心数据结构(链表、二叉树)全解析
数据结构·python·正则表达式
江屿风1 小时前
C++图论基础Bellman-Ford与spfa算法如何判断负环
开发语言·c++·笔记·算法·图论
森G1 小时前
68、项目配置和示例---------多媒体
c++·qt
lihao lihao1 小时前
Linux线程同步与互斥
linux·数据结构·算法
进击的荆棘1 小时前
优选算法——BFS
c++·算法·leetcode·宽度优先
.千余3 小时前
【C++】C++ set 与 multiset 完全指南:关联式容器入门
开发语言·c++·笔记·学习·其他