CodeTop按顺序刷题0003:LeetCode 206. 反转链表(迭代法图文详解与动态演示)
大家好!欢迎来到我的 CodeTop 按顺序刷题 合集。今天我们来攻克高频面试题:206. 反转链表。
在面试中,链表反转是极其基础且高频的手撕代码题。如果对指针的指向不够敏感,很容易在遍历时把链表弄"断"或者陷入死循环。今天我们用迭代法 来彻底搞懂它,并且文末附带了可交互的代码同步动画,带你一眼看清每一个指针的动态变化!
一、 题目描述与核心思想
- 题目链接 :LeetCode 206. 反转链表
- 题目要求 :给你单链表的头节点
head,请你反转链表,并返回反转后的链表。
核心思想:迭代法(双指针 + 临时指针)
假设链表为 1 → 2 → 3 → ∅ 1 \to 2 \to 3 \to \varnothing 1→2→3→∅,我们想要把它改成 ∅ ← 1 ← 2 ← 3 \varnothing \leftarrow 1 \leftarrow 2 \leftarrow 3 ∅←1←2←3。
在遍历链表时,将当前节点的 next 指针改为指向前一个节点。由于链表节点没有引用其前一个节点,因此我们必须:
- 事先存储其前一个节点 (
prev)。 - 在更改引用之前,必须先存储后一个节点 (
next),否则链表断开后后面的节点就找不到了! - 用
curr表示当前正在遍历的节点,循环推进直到curr变为空,最后返回新的头引用prev。
二、 C++ 完整代码
cpp
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr; // 前驱节点,初始化为 nullptr
ListNode* curr = head; // 当前节点,从头节点开始
while (curr) {
ListNode* next = curr->next; // 1. 暂存当前节点的下一个节点,防止链表断开丢失
curr->next = prev; // 2. 将当前节点的 next 指向 prev(实现反转)
prev = curr; // 3. prev 往后移(来到当前位置)
curr = next; // 4. curr 往后移(来到刚才暂存的 next 位置)
}
return prev; // 循环结束后,prev 指向新的头节点
}
};
三、 💡 核心可视化动画演示(HTML源码)
为了让你彻底看懂代码和指针是如何同步跑起来的,我制作了一个代码高亮与链表动态演变同步的交互式 HTML 动画。
你可以将下方代码复制并保存为 reverse_linked_list.html 文件,然后用任意浏览器双击打开,点击"下一步"亲自动手体验整个反转过程:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>单链表反转(迭代法)代码同步演示</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f7f9fa;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
margin: 0;
}
h2 { margin-bottom: 5px; color: #ffa116; }
.subtitle { font-size: 14px; color: #666; margin-bottom: 20px; }
.main-layout {
display: flex;
gap: 20px;
width: 900px;
max-width: 100%;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #1a1a1a;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 8px;
}
.container {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 15px;
min-height: 140px;
padding: 10px 0;
}
.node-wrapper {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.pointers {
height: 25px;
font-size: 11px;
font-weight: bold;
display: flex;
gap: 4px;
margin-bottom: 5px;
}
.ptr-tag {
padding: 1px 5px;
border-radius: 4px;
color: white;
display: none;
}
.ptr-prev { background-color: #ff4d4f; }
.ptr-curr { background-color: #1890ff; }
.ptr-next { background-color: #52c41a; }
.node-box {
display: flex;
align-items: center;
background: white;
border: 2px solid #d9d9d9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.node-val {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
border-right: 1px solid #d9d9d9;
}
.node-next {
width: 30px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: #8c8c8c;
}
.arrow {
font-size: 18px;
color: #8c8c8c;
font-weight: bold;
}
.null-node {
padding: 8px 12px;
background: #e8e8e8;
border-radius: 6px;
font-weight: bold;
font-size: 13px;
color: #595959;
}
.status-panel {
display: flex;
justify-content: space-around;
background: #f6f8fa;
padding: 10px;
border-radius: 6px;
margin-bottom: 15px;
font-size: 13px;
font-weight: bold;
}
.status-item span { font-weight: normal; color: #1890ff; }
pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 6px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.code-line {
display: block;
padding: 0 4px;
border-radius: 3px;
}
.code-line.active {
background-color: #264f78;
color: #ffffff;
font-weight: bold;
}
.controls {
display: flex;
gap: 15px;
margin-top: 20px;
justify-content: center;
}
button {
padding: 10px 25px;
font-size: 15px;
font-weight: bold;
border: none;
border-radius: 6px;
cursor: pointer;
background-color: #ffa116;
color: white;
box-shadow: 0 2px 6px rgba(255,161,22,0.3);
transition: background 0.2s;
}
button:hover { background-color: #e69110; }
button:disabled { background-color: #d9d9d9; cursor: not-allowed; box-shadow: none; }
.explanation {
margin: 15px auto 0 auto;
background: #fffbe6;
border: 1px solid #ffe58f;
padding: 12px 20px;
border-radius: 6px;
width: 860px;
font-size: 14px;
color: #d46b08;
line-height: 1.4;
}
</style>
</head>
<body>
<h2>LeetCode 206. 反转链表 - 代码与动画同步演示</h2>
<div class="subtitle">左侧看链表与指针动态变化,右侧看 C++ 代码实时高亮</div>
<div style="display: flex; justify-content: center;">
<div class="main-layout">
<div class="card">
<div class="card-title">🔗 链表可视化区</div>
<div class="status-panel" id="statusPanel">
<div class="status-item">prev: <span id="valPrev">nullptr</span></div>
<div class="status-item">curr: <span id="valCurr">1</span></div>
<div class="status-item">next: <span id="valNext">未定义</span></div>
</div>
<div class="container" id="linkedListContainer"></div>
</div>
<div class="card">
<div class="card-title">💻 C++ 代码同步高亮</div>
<pre><code><span id="line-3" class="code-line"> ListNode* reverseList(ListNode* head) {</span>
<span id="line-4" class="code-line"> ListNode* prev = nullptr;</span>
<span id="line-5" class="code-line"> ListNode* curr = head;</span>
<span id="line-6" class="code-line"> while (curr) {</span>
<span id="line-7" class="code-line"> ListNode* next = curr->next;</span>
<span id="line-8" class="code-line"> curr->next = prev;</span>
<span id="line-9" class="code-line"> prev = curr;</span>
<span id="line-10" class="code-line"> curr = next;</span>
<span id="line-11" class="code-line"> }</span>
<span id="line-12" class="code-line"> return prev;</span>
<span id="line-13" class="code-line"> }</span></code></pre>
</div>
</div>
</div>
<div class="controls">
<button id="prevStepBtn" onclick="prevStep()" disabled>上一步</button>
<button id="nextStepBtn" onclick="nextStep()">下一步</button>
<button onclick="resetDemo()">重置</button>
</div>
<div class="explanation" id="explanationText" style="max-width: 860px; box-sizing: border-box;">
💡 准备就绪:点击"下一步"开始执行初始化代码。
</div>
<script>
let steps = [
{
line: 4,
state: [
{ val: 1, nextPtr: 2, isReversed: false },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: null, curr: 0, next: null },
textVals: { prev: "nullptr", curr: "节点 1", next: "nullptr (未初始化)" },
text: "执行第 4 行:初始化 prev 指针指向 nullptr。"
},
{
line: 5,
state: [
{ val: 1, nextPtr: 2, isReversed: false },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: null, curr: 0, next: null },
textVals: { prev: "nullptr", curr: "节点 1", next: "nullptr (未初始化)" },
text: "执行第 5 行:初始化 curr 指针指向头节点 1。"
},
{
line: 6,
state: [
{ val: 1, nextPtr: 2, isReversed: false },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: null, curr: 0, next: null },
textVals: { prev: "nullptr", curr: "节点 1", next: "nullptr" },
text: "进入 while(curr) 循环:curr 不为空(当前是节点 1),开始执行循环体。"
},
{
line: 7,
state: [
{ val: 1, nextPtr: 2, isReversed: false },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: null, curr: 0, next: 1 },
textVals: { prev: "nullptr", curr: "节点 1", next: "节点 2" },
text: "执行第 7 行 [ListNode* next = curr->next]:用 next 暂存节点 2,防止断开后找不到了!"
},
{
line: 8,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: null, curr: 0, next: 1 },
textVals: { prev: "nullptr", curr: "节点 1", next: "节点 2" },
text: "执行第 8 行 [curr->next = prev]:修改节点 1 的 next 指向 prev(nullptr),实现箭头反转。"
},
{
line: 9,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 0, curr: 0, next: 1 },
textVals: { prev: "节点 1", curr: "节点 1", next: "节点 2" },
text: "执行第 9 行 [prev = curr]:prev 往右移,变成当前节点 1。"
},
{
line: 10,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 0, curr: 1, next: 1 },
textVals: { prev: "节点 1", curr: "节点 2", next: "节点 2" },
text: "执行第 10 行 [curr = next]:curr 往右移,通过暂存的 next 走到节点 2。第一轮循环结束!"
},
{
line: 6,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 0, curr: 1, next: 1 },
textVals: { prev: "节点 1", curr: "节点 2", next: "节点 2" },
text: "再次进入 while 循环:curr 现在是节点 2(不为空)。"
},
{
line: 7,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 3, isReversed: false },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 0, curr: 1, next: 2 },
textVals: { prev: "节点 1", curr: "节点 2", next: "节点 3" },
text: "执行第 7 行:用 next 暂存节点 2 的下一个节点(即节点 3)。"
},
{
line: 8,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 0, curr: 1, next: 2 },
textVals: { prev: "节点 1", curr: "节点 2", next: "节点 3" },
text: "执行第 8 行:把节点 2 的 next 指向 prev(节点 1),实现反转。"
},
{
line: 9,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 1, curr: 1, next: 2 },
textVals: { prev: "节点 2", curr: "节点 2", next: "节点 3" },
text: "执行第 9 行:prev 往右移动到节点 2。"
},
{
line: 10,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 1, curr: 2, next: 2 },
textVals: { prev: "节点 2", curr: "节点 3", next: "节点 3" },
text: "执行第 10 行:curr 往右移动到节点 3。第二轮循环结束!"
},
{
line: 6,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 1, curr: 2, next: 2 },
textVals: { prev: "节点 2", curr: "节点 3", next: "节点 3" },
text: "再次进入 while 循环:curr 现在是节点 3(不为空)。"
},
{
line: 7,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 'null', isReversed: false }
],
pointers: { prev: 1, curr: 2, next: 'null' },
textVals: { prev: "节点 2", curr: "节点 3", next: "nullptr" },
text: "执行第 7 行:用 next 暂存节点 3 的下一个节点(nullptr)。"
},
{
line: 8,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 2, isReversed: true }
],
pointers: { prev: 1, curr: 2, next: 'null' },
textVals: { prev: "节点 2", curr: "节点 3", next: "nullptr" },
text: "执行第 8 行:把节点 3 的 next 指向 prev(节点 2),实现反转。"
},
{
line: 9,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 2, isReversed: true }
],
pointers: { prev: 2, curr: 2, next: 'null' },
textVals: { prev: "节点 3", curr: "节点 3", next: "nullptr" },
text: "执行第 9 行:prev 往右移动到节点 3。"
},
{
line: 10,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 2, isReversed: true }
],
pointers: { prev: 2, curr: null, next: 'null' },
textVals: { prev: "节点 3", curr: "nullptr", next: "nullptr" },
text: "执行第 10 行:curr 走到 next 的位置(nullptr)。第三轮循环结束!"
},
{
line: 6,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 2, isReversed: true }
],
pointers: { prev: 2, curr: null, next: 'null' },
textVals: { prev: "节点 3", curr: "nullptr", next: "nullptr" },
text: "再次判断 while(curr):此时 curr 为 nullptr,条件不成立,跳出循环。"
},
{
line: 12,
state: [
{ val: 1, nextPtr: 'null', isReversed: true },
{ val: 2, nextPtr: 1, isReversed: true },
{ val: 3, nextPtr: 2, isReversed: true }
],
pointers: { prev: 2, curr: null, next: 'null' },
textVals: { prev: "节点 3", curr: "nullptr", next: "nullptr" },
text: "执行第 12 行 [return prev]:返回新的头节点(节点 3)。链表反转成功!"
}
];
let currentStepIndex = 0;
function render() {
const step = steps[currentStepIndex];
document.querySelectorAll('.code-line').forEach(el => el.classList.remove('active'));
const activeLineEl = document.getElementById(`line-${step.line}`);
if (activeLineEl) activeLineEl.classList.add('active');
document.getElementById('valPrev').innerText = step.textVals.prev;
document.getElementById('valCurr').innerText = step.textVals.curr;
document.getElementById('valNext').innerText = step.textVals.next;
const container = document.getElementById('linkedListContainer');
container.innerHTML = '';
step.state.forEach((node, idx) => {
const wrapper = document.createElement('div');
wrapper.className = 'node-wrapper';
const ptrsDiv = document.createElement('div');
ptrsDiv.className = 'pointers';
if (step.pointers.prev === idx) {
const p = document.createElement('span');
p.className = 'ptr-tag ptr-prev';
p.style.display = 'inline-block';
p.innerText = 'prev';
ptrsDiv.appendChild(p);
}
if (step.pointers.curr === idx) {
const c = document.createElement('span');
c.className = 'ptr-tag ptr-curr';
c.style.display = 'inline-block';
c.innerText = 'curr';
ptrsDiv.appendChild(c);
}
if (step.pointers.next === idx) {
const n = document.createElement('span');
n.className = 'ptr-tag ptr-next';
n.style.display = 'inline-block';
n.innerText = 'next';
ptrsDiv.appendChild(n);
}
wrapper.appendChild(ptrsDiv);
const box = document.createElement('div');
box.className = 'node-box';
box.innerHTML = `<div class="node-val">${node.val}</div><div class="node-next">next</div>`;
wrapper.appendChild(box);
container.appendChild(wrapper);
if (idx < step.state.length - 1) {
const arrow = document.createElement('div');
arrow.className = 'arrow';
if (node.isReversed) {
arrow.innerHTML = '←';
arrow.style.color = '#ff4d4f';
} else {
arrow.innerHTML = '→';
}
container.appendChild(arrow);
}
});
const nullNode = document.createElement('div');
nullNode.className = 'null-node';
nullNode.innerText = 'Ø (null)';
container.appendChild(nullNode);
document.getElementById('explanationText').innerText = step.text;
document.getElementById('prevStepBtn').disabled = currentStepIndex === 0;
document.getElementById('nextStepBtn').disabled = currentStepIndex === steps.length - 1;
}
function nextStep() {
if (currentStepIndex < steps.length - 1) {
currentStepIndex++;
render();
}
}
function prevStep() {
if (currentStepIndex > 0) {
currentStepIndex--;
render();
}
}
function resetDemo() {
currentStepIndex = 0;
render();
}
render();
</script>
</body>
</html>
四、 复杂度分析
- 时间复杂度 : O ( n ) O(n) O(n),其中 n n n 是链表的长度。需要遍历链表中的每个节点一次。
- 空间复杂度 : O ( 1 ) O(1) O(1),只需要常数级别的额外空间来存储
prev、curr和next指针。
希望这篇图文和动态演示能帮你彻底搞懂反转链表的迭代写法!欢迎大家关注我的 CodeTop 按顺序刷题 合集,我们下题再见!