Files
illustrated-algorithm/swap-nodes-in-pairs/index.html
T

852 lines
33 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>030. 两两交换链表中的节点 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 200px; padding: 16px 0; }
.code-section { margin-top: 16px; }
/* ---- SVG Linked List ---- */
.ll-svg-wrap { overflow-x: auto; padding: 8px 0; }
.ll-svg-wrap svg { display: block; min-width: 400px; }
/* ---- Swap Focus Diagram ---- */
.swap-focus {
margin-top: 20px; padding: 16px; background: #f8fafc;
border: 2px solid #e2e8f0; border-radius: 12px; overflow-x: auto;
}
.swap-focus .op-badge {
display: inline-block; padding: 4px 12px; border-radius: 999px;
font-size: 13px; font-weight: 700; margin-bottom: 12px;
}
.swap-focus .op-badge.remove {
background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5;
}
.swap-focus .op-badge.add {
background: #dcfce7; color: #166534; border: 1px solid #86efac;
}
.swap-focus svg { display: block; margin-top: 8px; }
/* ---- Legend ---- */
.legend-row {
display: flex; flex-wrap: wrap; gap: 14px; margin: 10px 0 4px; font-size: 12px; color: var(--text-secondary);
}
.legend-row span { display: inline-flex; align-items: center; gap: 4px; }
.legend-dot {
width: 14px; height: 3px; border-radius: 2px; display: inline-block;
}
.legend-dot.normal { background: var(--blue); }
.legend-dot.removed { background: var(--red); border-top: 2px dashed var(--red); height: 0; }
.legend-dot.added { background: var(--green); height: 4px; }
/* ---- Pointer key ---- */
.ptr-key {
display: flex; flex-wrap: wrap; gap: 10px; margin: 8px 0; font-size: 12px;
}
.ptr-key span { display: inline-flex; align-items: center; gap: 3px; }
.ptr-dot {
width: 10px; height: 10px; border-radius: 3px; display: inline-block;
}
.ptr-dot.c-prev { background: #166534; }
.ptr-dot.c-first { background: #92400e; }
.ptr-dot.c-second { background: #5b21b6; }
.ptr-dot.c-temp { background: #155e75; }
</style>
</head>
<body>
<div class="container">
<h1>🟡 030. 两两交换链表中的节点 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:链表 | LeetCode Hot 100</p>
<!-- 控制面板 -->
<div class="controls" id="controls">
<label for="inputArea">输入:</label>
<input type="text" id="inputArea" value="head=[1,2,3,4]" placeholder="head=[1,2,3,4]">
<button id="applyBtn" class="primary">生成图解</button>
<select id="exampleSelect"></select>
<span style="flex:1"></span>
<button id="prevBtn">◀ 上一步</button>
<button id="nextBtn">下一步 ▶</button>
<button id="jumpBtn">⏭ 跳到结果</button>
<button id="autoBtn">自动播放</button>
<button id="resetBtn">重置</button>
</div>
<!-- 步骤流水线 -->
<div class="pipeline" id="pipeline"></div>
<!-- 提示条 -->
<div class="hint info" id="hintBox">
<span id="stepInfo"></span><br>
<span id="hintText">点击「生成图解」开始</span>
</div>
<!-- 可视化面板 -->
<div class="panels">
<div class="panel" id="mainPanel">
<h3>📊 可视化 — 链表结构与指针变化</h3>
<div class="vis-area" id="vizArea">点击「生成图解」开始</div>
</div>
<div class="panel-grid">
<div class="panel" id="detailPanel">
<h3>📝 当前步骤详情</h3>
<div id="detailContent">等待开始...</div>
</div>
<div class="panel" id="resultPanel">
<h3>✅ 结果</h3>
<div id="resultContent">等待完成...</div>
</div>
</div>
</div>
<!-- 代码 -->
<div class="panel code-section">
<h3>💻 参考代码(Python)</h3>
<div id="codeArea"></div>
</div>
<footer>Powered by QwenPaw · 图解算法 · LeetCode Hot 100</footer>
</div>
<script src="../shared/algo-viz.js"></script>
<script>
"use strict";
(function() {
// ========== Constants ==========
const PIPELINE_STAGES = ['初始化', '识别节点对', '三步交换', '前进', '完成'];
const CODE = `class Solution:
def swapPairs(self, head):
dummy = ListNode(0, head)
prev = dummy
while prev.next and prev.next.next:
first = prev.next
second = prev.next.next
first.next = second.next
second.next = first
prev.next = second
prev = first
return dummy.next`;
const examples = [
{ input: [1,2,3,4], label: '示例1: [1,2,3,4] → [2,1,4,3]' },
{ input: [], label: '示例2: [] → []' },
{ input: [1], label: '示例3: [1] → [1]' },
{ input: [1,2,3], label: '示例4: [1,2,3] → [2,1,3]' },
{ input: [1,2], label: '示例5: [1,2] → [2,1]' },
];
// ========== State ==========
let steps = [], stepCtrl;
// ========== Linked List Simulation ==========
// Nodes: index 0 = dummy(val='D'), 1..n = value nodes, nullId = n+1
function simulate(values) {
const nullId = values.length + 1;
const nodes = []; // { id, val, next }
const nextMap = {}; // id → nextId
// Build initial list
nodes.push({ id: 0, val: 'D', next: values.length > 0 ? 1 : nullId });
nextMap[0] = values.length > 0 ? 1 : nullId;
for (let i = 0; i < values.length; i++) {
const id = i + 1;
nodes.push({ id, val: values[i], next: id + 1 });
nextMap[id] = id + 1;
}
function snapshot() { return Object.assign({}, nextMap); }
function traversal() {
const order = [];
let cur = nextMap[0];
while (cur !== nullId) { order.push(cur); cur = nextMap[cur]; }
return order;
}
function valOf(id) { return id === nullId ? 'NULL' : id === 0 ? 'D' : nodes[id].val; }
const stepsArr = [];
// ---- Step: Initialize ----
stepsArr.push({
stage: '初始化',
msg: '创建虚拟头节点 dummy,令 prev = dummy',
detail: 'dummy 指向链表头部,prev 从 dummy 开始遍历\n虚拟头节点简化了头节点交换时的边界处理,无需特殊判断',
nextMap: snapshot(),
ptrs: { prev: 0 },
highlights: { 0: 'prev' },
arrowOps: [],
codeLines: [3, 4],
swapFocus: null,
});
let prevId = 0;
while (true) {
const firstId = nextMap[prevId];
const secondId = firstId !== nullId ? nextMap[firstId] : nullId;
// Check: need two nodes to swap
if (firstId === nullId || secondId === nullId) {
const reason = firstId === nullId
? `prev.next 为 NULL,不足两个节点`
: `prev.next.next 为 NULL,只剩一个节点 ${valOf(firstId)},无需交换`;
stepsArr.push({
stage: '完成',
msg: `循环条件不满足:${reason}`,
detail: `while 条件: prev.next and prev.next.next\n${reason}\n循环结束,返回 dummy.next`,
nextMap: snapshot(),
ptrs: { prev: prevId },
highlights: {},
arrowOps: [],
codeLines: [5, 12],
swapFocus: null,
});
break;
}
const tempId = nextMap[secondId];
// ---- Step: Identify pair ----
stepsArr.push({
stage: '识别节点对',
msg: `识别节点对:first=${valOf(firstId)}, second=${valOf(secondId)}, 保存 temp=${valOf(tempId)}`,
detail: `prev.next = ${valOf(firstId)} ≠ NULL ✓\nprev.next.next = ${valOf(secondId)} ≠ NULL ✓\n可以交换!\n\nfirst = prev.next = ${valOf(firstId)}\nsecond = prev.next.next = ${valOf(secondId)}\ntemp = second.next = ${valOf(tempId)}`,
nextMap: snapshot(),
ptrs: { prev: prevId, first: firstId, second: secondId, temp: tempId },
highlights: { [prevId]: 'prev', [firstId]: 'first', [secondId]: 'second', [tempId]: 'temp' },
arrowOps: [],
codeLines: [5, 6, 7],
swapFocus: null,
});
// ---- Swap Step 1: first.next = second.next ----
const oldFirstNext = nextMap[firstId];
nextMap[firstId] = tempId;
stepsArr.push({
stage: '三步交换',
msg: `第一步:first.next = second.next → ${valOf(firstId)}.next = ${valOf(tempId)}`,
detail: `断开 first(${valOf(firstId)}) → second(${valOf(secondId)}) 的连接\n建立 first(${valOf(firstId)}) → temp(${valOf(tempId)}) 的新连接\n\n保存了 second.next 后,first 不再指向 second\n这样 second 才能安全地指向 first`,
nextMap: snapshot(),
ptrs: { prev: prevId, first: firstId, second: secondId, temp: tempId },
highlights: { [prevId]: 'prev', [firstId]: 'first', [secondId]: 'second', [tempId]: 'temp' },
arrowOps: [
{ from: firstId, to: oldFirstNext, type: 'remove' },
{ from: firstId, to: tempId, type: 'add' },
],
codeLines: [8],
swapFocus: { op: `first.next = second.next`, prevId, firstId, secondId, tempId,
removed: [{ from: firstId, to: secondId }],
added: [{ from: firstId, to: tempId }],
},
});
// ---- Swap Step 2: second.next = first ----
const oldSecondNext = nextMap[secondId];
nextMap[secondId] = firstId;
stepsArr.push({
stage: '三步交换',
msg: `第二步:second.next = first → ${valOf(secondId)}.next = ${valOf(firstId)}`,
detail: `断开 second(${valOf(secondId)}) → temp(${valOf(tempId)}) 的连接\n建立 second(${valOf(secondId)}) → first(${valOf(firstId)}) 的新连接\n\n这是交换的核心:second 反过来指向 first\n完成这一步后,second→first 链已形成`,
nextMap: snapshot(),
ptrs: { prev: prevId, first: firstId, second: secondId, temp: tempId },
highlights: { [prevId]: 'prev', [firstId]: 'first', [secondId]: 'second', [tempId]: 'temp' },
arrowOps: [
{ from: secondId, to: oldSecondNext, type: 'remove' },
{ from: secondId, to: firstId, type: 'add' },
],
codeLines: [9],
swapFocus: { op: `second.next = first`, prevId, firstId, secondId, tempId,
removed: [{ from: secondId, to: tempId }],
added: [{ from: secondId, to: firstId }],
},
});
// ---- Swap Step 3: prev.next = second ----
const oldPrevNext = nextMap[prevId];
nextMap[prevId] = secondId;
stepsArr.push({
stage: '三步交换',
msg: `第三步:prev.next = second → ${valOf(prevId)}.next = ${valOf(secondId)}`,
detail: `断开 prev(${valOf(prevId)}) → first(${valOf(firstId)}) 的连接\n建立 prev(${valOf(prevId)}) → second(${valOf(secondId)}) 的新连接\n\n这一步将交换后的子链重新接入主链\n交换完成!新顺序:${valOf(prevId)} → ${valOf(secondId)} → ${valOf(firstId)} → ${valOf(tempId)}`,
nextMap: snapshot(),
ptrs: { prev: prevId, first: firstId, second: secondId, temp: tempId },
highlights: { [prevId]: 'prev', [firstId]: 'first', [secondId]: 'second', [tempId]: 'temp' },
arrowOps: [
{ from: prevId, to: oldPrevNext, type: 'remove' },
{ from: prevId, to: secondId, type: 'add' },
],
codeLines: [10],
swapFocus: { op: `prev.next = second`, prevId, firstId, secondId, tempId,
removed: [{ from: prevId, to: firstId }],
added: [{ from: prevId, to: secondId }],
},
});
// ---- Advance: prev = first ----
prevId = firstId;
nextMap[prevId]; // just to reference
stepsArr.push({
stage: '前进',
msg: `前进:prev = first = ${valOf(firstId)}`,
detail: `prev 移动到已交换好的前一个位置 (= first)\n此时 prev 后面的两个节点将是下一对要交换的\n\n当前链表:${(() => {
const t = traversal(); return 'D → ' + t.map(id => valOf(id)).join(' → ') + ' → NULL';
})()}`,
nextMap: snapshot(),
ptrs: { prev: prevId },
highlights: { [prevId]: 'prev' },
arrowOps: [],
codeLines: [11],
swapFocus: null,
});
}
// ---- Final Result ----
const result = traversal();
const resultStr = result.length > 0 ? result.map(id => valOf(id)).join(' → ') + ' → NULL' : 'NULL (空链表)';
// Only add if the last step wasn't already the "完成" stage
const lastStep = stepsArr[stepsArr.length - 1];
if (lastStep.stage !== '完成' || !lastStep.isFinal) {
stepsArr.push({
stage: '完成',
msg: `返回 dummy.next,交换完成!`,
detail: `最终结果:${resultStr}\n\n每个相邻节点对都已两两交换\n虚拟头节点 dummy 的作用:\n • 统一头节点的处理逻辑\n • 避免 prev 为 None 的边界判断`,
nextMap: snapshot(),
ptrs: {},
highlights: {},
arrowOps: [],
codeLines: [12],
swapFocus: null,
isFinal: true,
result: result.map(id => valOf(id)),
});
}
return { stepsArr, nodes, nullId };
}
// ========== Rendering ==========
// --- Pipeline ---
function renderPipeline(activeStage) {
let html = '';
PIPELINE_STAGES.forEach((s, i) => {
if (i > 0) html += '<i>→</i>';
html += `<span class="pipe-step${s === activeStage ? ' active' : ''}">${s}</span>`;
});
$('pipeline').innerHTML = html;
}
// --- Main SVG Linked List ---
function renderListSVG(nextMap, nodes, nullId, ptrs, highlights, arrowOps) {
const nodeW = 68, nodeH = 40, gapX = 40, startY = 56;
const maxPerRow = 6;
const rowGap = 80;
const labelH = 36;
// Determine all node IDs to draw (always include dummy + all value nodes + null)
const allIds = [0];
for (let i = 1; i < nullId; i++) allIds.push(i);
allIds.push(nullId);
// Calculate positions
const posMap = {};
let x = 10, y = startY;
allIds.forEach((id, idx) => {
if (idx > 0 && idx % maxPerRow === 0) { x = 10; y += rowGap + labelH; }
posMap[id] = { x, y };
x += nodeW + gapX;
});
const svgW = Math.max(420, x + 10);
const svgH = y + nodeH + labelH + 20;
const arrowMarkerDefs = `
<defs>
<marker id="ah-normal" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#3b82f6"/>
</marker>
<marker id="ah-added" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#16a34a"/>
</marker>
<marker id="ah-removed" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#ef4444" opacity="0.5"/>
</marker>
<marker id="ah-null" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#94a3b8"/>
</marker>
</defs>`;
let svg = `<svg width="100%" viewBox="0 0 ${svgW} ${svgH}" xmlns="http://www.w3.org/2000/svg" style="font-family:inherit;">`;
svg += arrowMarkerDefs;
// --- Draw arrows layer (behind nodes) ---
// Collect arrow operations for quick lookup
const removeSet = new Set();
const addSet = new Set();
if (arrowOps) {
arrowOps.forEach(op => {
const key = op.from + '->' + op.to;
if (op.type === 'remove') removeSet.add(key);
if (op.type === 'add') addSet.add(key);
});
}
// Draw normal arrows from nextMap (skip ones being removed)
const drawnKeys = new Set();
Object.entries(nextMap).forEach(([fromStr, toId]) => {
const fromId = parseInt(fromStr);
// Only draw from non-null nodes (and skip nodes past the end)
if (fromId > nullId || fromId === nullId) return;
const key = fromId + '->' + toId;
// Skip if this arrow is being removed (we'll draw it differently)
if (removeSet.has(key)) return;
// Skip if this is going to be drawn as "added"
// (We draw added arrows on top later)
if (addSet.has(key)) return;
const fromPos = posMap[fromId];
if (!fromPos) return;
if (toId === nullId) {
// Arrow to NULL
const nullPos = posMap[nullId];
if (nullPos) {
svg += drawArrowPath(fromPos, nullPos, nodeW, nodeH, 'null');
}
} else {
const toPos = posMap[toId];
if (toPos) {
drawnKeys.add(key);
svg += drawArrowPath(fromPos, toPos, nodeW, nodeH, 'normal');
}
}
});
// Draw removed arrows (red dashed)
if (arrowOps) {
arrowOps.filter(op => op.type === 'remove').forEach(op => {
const fromPos = posMap[op.from];
const toPos = posMap[op.to];
if (fromPos && toPos) {
svg += drawArrowPath(fromPos, toPos, nodeW, nodeH, 'removed');
}
});
}
// Draw added arrows (green bold)
if (arrowOps) {
arrowOps.filter(op => op.type === 'add').forEach(op => {
const fromPos = posMap[op.from];
const toPos = posMap[op.to];
if (fromPos && toPos) {
svg += drawArrowPath(fromPos, toPos, nodeW, nodeH, 'added');
}
});
}
// --- Draw nodes ---
allIds.forEach(id => {
const p = posMap[id];
const hl = highlights[id] || '';
const isNull = id === nullId;
const isDummy = id === 0;
const val = isNull ? 'NULL' : isDummy ? 'D' : nodes[id].val;
// Node rect
let fill, stroke, textFill, rx = 8;
if (isNull) {
fill = '#f1f5f9'; stroke = '#94a3b8'; textFill = '#94a3b8';
rx = 4;
} else if (hl === 'prev') {
fill = '#dcfce7'; stroke = '#16a34a'; textFill = '#166534';
} else if (hl === 'first') {
fill = '#fef3c7'; stroke = '#f59e0b'; textFill = '#92400e';
} else if (hl === 'second') {
fill = '#ede9fe'; stroke = '#8b5cf6'; textFill = '#5b21b6';
} else if (hl === 'temp') {
fill = '#cffafe'; stroke = '#06b6d4'; textFill = '#155e75';
} else if (isDummy) {
fill = '#f1f5f9'; stroke = '#64748b'; textFill = '#475569';
} else {
fill = '#dbeafe'; stroke = '#3b82f6'; textFill = '#1e40af';
}
if (isNull) {
svg += `<rect x="${p.x}" y="${p.y}" width="${nodeW - 20}" height="${nodeH}" rx="${rx}" fill="${fill}" stroke="${stroke}" stroke-width="1.5" stroke-dasharray="5,3"/>`;
svg += `<text x="${p.x + (nodeW - 20)/2}" y="${p.y + nodeH/2 + 5}" text-anchor="middle" fill="${textFill}" font-size="12" font-weight="600" font-style="italic">${val}</text>`;
} else {
svg += `<rect x="${p.x}" y="${p.y}" width="${nodeW}" height="${nodeH}" rx="${rx}" fill="${fill}" stroke="${stroke}" stroke-width="2"/>`;
svg += `<text x="${p.x + nodeW/2}" y="${p.y + nodeH/2 + 5}" text-anchor="middle" fill="${textFill}" font-size="15" font-weight="700">${val}</text>`;
// Draw the "next" cell separator
svg += `<line x1="${p.x + nodeW - 22}" y1="${p.y}" x2="${p.x + nodeW - 22}" y2="${p.y + nodeH}" stroke="${stroke}" stroke-width="1" opacity="0.3"/>`;
svg += `<text x="${p.x + nodeW - 11}" y="${p.y + nodeH/2 + 4}" text-anchor="middle" fill="${stroke}" font-size="13" opacity="0.5">•</text>`;
// Highlight glow
if (hl === 'prev' || hl === 'first' || hl === 'second' || hl === 'temp') {
svg += `<rect x="${p.x - 3}" y="${p.y - 3}" width="${nodeW + 6}" height="${nodeH + 6}" rx="${rx + 2}" fill="none" stroke="${stroke}" stroke-width="1.5" opacity="0.25"/>`;
}
}
});
// --- Draw pointer labels below nodes ---
const ptrEntries = Object.entries(ptrs);
if (ptrEntries.length > 0) {
allIds.forEach(id => {
const p = posMap[id];
if (!p) return;
const labels = ptrEntries.filter(([_, nodeId]) => nodeId === id).map(([name]) => name);
if (labels.length === 0) return;
const isNull = id === nullId;
const cx = isNull ? p.x + (nodeW - 20)/2 : p.x + nodeW/2;
const cy = p.y + nodeH + 14;
labels.forEach((label, li) => {
let color;
switch(label) {
case 'prev': color = '#16a34a'; break;
case 'first': color = '#f59e0b'; break;
case 'second': color = '#8b5cf6'; break;
case 'temp': color = '#06b6d4'; break;
default: color = '#64748b';
}
// Small triangle pointer
svg += `<polygon points="${cx},${cy - 10} ${cx - 5},${cy - 15} ${cx + 5},${cy - 15}" fill="${color}"/>`;
svg += `<text x="${cx}" y="${cy + li * 14}" text-anchor="middle" fill="${color}" font-size="11" font-weight="700">${label}</text>`;
});
});
}
svg += '</svg>';
return svg;
}
// Draw an SVG arrow path between two node positions
function drawArrowPath(fromPos, toPos, nodeW, nodeH, style, nullIdOpt) {
// Arrow start: right center of source node (source is never null)
const x1 = fromPos.x + nodeW;
const y1 = fromPos.y + nodeH / 2;
// Arrow end: left center of target node
const x2 = toPos.x;
const y2 = toPos.y + nodeH / 2;
// Determine if we need a curve
// Same row: forward arrow
// Different row: curve down/up
// Backward: curve above
const sameRow = Math.abs(y1 - y2) < 5;
const forward = x2 > x1 + 10;
let pathD;
if (sameRow && forward) {
// Straight arrow
pathD = `M ${x1} ${y1} L ${x2} ${y2}`;
} else if (sameRow && !forward) {
// Backward arrow on same row — curve above
const midX = (x1 + x2) / 2;
const curveH = Math.max(30, Math.abs(x2 - x1) * 0.4);
pathD = `M ${x1} ${y1} Q ${midX} ${y1 - curveH} ${x2} ${y2}`;
} else {
// Different row — curve
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2 - 20;
pathD = `M ${x1} ${y1} Q ${midX} ${midY} ${x2} ${y2}`;
}
let strokeColor, strokeWidth, dashArray, opacity, markerId;
switch(style) {
case 'normal':
strokeColor = '#3b82f6'; strokeWidth = 2; dashArray = ''; opacity = 0.85; markerId = 'ah-normal'; break;
case 'removed':
strokeColor = '#ef4444'; strokeWidth = 2; dashArray = '6,4'; opacity = 0.5; markerId = 'ah-removed'; break;
case 'added':
strokeColor = '#16a34a'; strokeWidth = 3; dashArray = ''; opacity = 1; markerId = 'ah-added'; break;
case 'null':
strokeColor = '#94a3b8'; strokeWidth = 1.5; dashArray = '4,3'; opacity = 0.6; markerId = 'ah-null'; break;
default:
strokeColor = '#94a3b8'; strokeWidth = 1.5; dashArray = ''; opacity = 0.5; markerId = 'ah-normal';
}
return `<path d="${pathD}" fill="none" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-dasharray="${dashArray}" opacity="${opacity}" marker-end="url(#${markerId})"/>`;
}
// --- Swap Focus Diagram ---
function renderSwapFocus(focus, nodes, nullId) {
if (!focus) return '';
const { op, prevId, firstId, secondId, tempId, removed, added } = focus;
function v(id) { return id === nullId ? 'NULL' : id === 0 ? 'D' : nodes[id].val; }
function label(id) {
if (id === prevId) return 'prev';
if (id === firstId) return 'first';
if (id === secondId) return 'second';
if (id === tempId) return 'temp';
return '';
}
const nodeW = 64, nodeH = 38, gapX = 56;
const startY = 44;
const ids = [prevId, firstId, secondId, tempId];
const positions = {};
ids.forEach((id, i) => { positions[id] = { x: 16 + i * (nodeW + gapX), y: startY }; });
const svgW = 16 + ids.length * (nodeW + gapX) - gapX + 16;
const svgH = startY + nodeH + 50;
const defs = `
<defs>
<marker id="sf-ah-normal" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#94a3b8"/>
</marker>
<marker id="sf-ah-removed" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#ef4444" opacity="0.6"/>
</marker>
<marker id="sf-ah-added" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
<path d="M0,0 L10,4 L0,8 Z" fill="#16a34a"/>
</marker>
</defs>`;
let svg = `<svg width="100%" viewBox="0 0 ${svgW} ${svgH}" xmlns="http://www.w3.org/2000/svg" style="font-family:inherit;">`;
svg += defs;
// Draw unchanged arrows (gray, between adjacent nodes from left to right)
const removedFromSet = new Set(removed.map(r => r.from));
const addedFromSet = new Set(added.map(a => a.from));
for (let i = 0; i < ids.length - 1; i++) {
const fromId = ids[i], toId = ids[i + 1];
const isRemoved = removed.some(r => r.from === fromId && r.to === toId);
const isAdded = added.some(a => a.from === fromId && a.to === toId);
if (!isRemoved && !isAdded) {
// Unchanged connection
const fp = positions[fromId], tp = positions[toId];
const x1 = fp.x + nodeW, y1 = fp.y + nodeH / 2;
const x2 = tp.x, y2 = tp.y + nodeH / 2;
svg += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#94a3b8" stroke-width="2" marker-end="url(#sf-ah-normal)"/>`;
}
}
// Draw removed arrows
removed.forEach(r => {
const fp = positions[r.from], tp = positions[r.to];
if (!fp || !tp) return;
const x1 = fp.x + nodeW, y1 = fp.y + nodeH / 2;
const x2 = tp.x, y2 = tp.y + nodeH / 2;
// For backward or non-adjacent arrows, use a curve
if (x2 <= x1 + 10) {
const midX = (x1 + x2) / 2;
svg += `<path d="M ${x1} ${y1} Q ${midX} ${y1 - 28} ${x2} ${y2}" fill="none" stroke="#ef4444" stroke-width="2" stroke-dasharray="6,4" opacity="0.5" marker-end="url(#sf-ah-removed)"/>`;
} else {
svg += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#ef4444" stroke-width="2" stroke-dasharray="6,4" opacity="0.5" marker-end="url(#sf-ah-removed)"/>`;
}
// X mark on the removed arrow
const midX = (x1 + x2) / 2, midY = (y1 + y2) / 2;
svg += `<text x="${midX}" y="${midY - 8}" text-anchor="middle" fill="#ef4444" font-size="14" font-weight="800">✕</text>`;
});
// Draw added arrows
added.forEach(a => {
const fp = positions[a.from], tp = positions[a.to];
if (!fp || !tp) return;
const x1 = fp.x + nodeW, y1 = fp.y + nodeH / 2;
const x2 = tp.x, y2 = tp.y + nodeH / 2;
// Determine offset to not overlap with removed arrow
let yOffset = 0;
const isRemovedSameDirection = removed.some(r => r.from === a.from);
if (isRemovedSameDirection) yOffset = -14; // curve above to show new arrow
if (x2 <= x1 + 10 || yOffset !== 0) {
// Curved arrow (backward or offset from removed)
const midX = (x1 + x2) / 2;
const curveOffset = Math.max(28, Math.abs(x1 - x2) * 0.3) + Math.abs(yOffset);
svg += `<path d="M ${x1} ${y1} Q ${midX} ${y1 - curveOffset} ${x2} ${y2}" fill="none" stroke="#16a34a" stroke-width="3" marker-end="url(#sf-ah-added)"/>`;
} else {
svg += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#16a34a" stroke-width="3" marker-end="url(#sf-ah-added)"/>`;
}
});
// Draw nodes
ids.forEach(id => {
const p = positions[id];
const isNull = id === nullId;
const isDummyNode = id === 0;
const val = v(id);
let fill, stroke, textFill;
if (id === prevId) { fill = '#dcfce7'; stroke = '#16a34a'; textFill = '#166534'; }
else if (id === firstId) { fill = '#fef3c7'; stroke = '#f59e0b'; textFill = '#92400e'; }
else if (id === secondId) { fill = '#ede9fe'; stroke = '#8b5cf6'; textFill = '#5b21b6'; }
else if (id === tempId) { fill = '#cffafe'; stroke = '#06b6d4'; textFill = '#155e75'; }
else if (isNull) { fill = '#f1f5f9'; stroke = '#94a3b8'; textFill = '#94a3b8'; }
else if (isDummyNode){ fill = '#f1f5f9'; stroke = '#64748b'; textFill = '#475569'; }
else { fill = '#dbeafe'; stroke = '#3b82f6'; textFill = '#1e40af'; }
const w = isNull ? nodeW - 16 : nodeW;
svg += `<rect x="${p.x}" y="${p.y}" width="${w}" height="${nodeH}" rx="8" fill="${fill}" stroke="${stroke}" stroke-width="2"/>`;
svg += `<text x="${p.x + w/2}" y="${p.y + nodeH/2 + 5}" text-anchor="middle" fill="${textFill}" font-size="14" font-weight="700">${val}</text>`;
// Label below
const lb = label(id);
if (lb) {
svg += `<text x="${p.x + w/2}" y="${p.y + nodeH + 14}" text-anchor="middle" fill="${stroke}" font-size="11" font-weight="700">${lb}</text>`;
}
});
svg += '</svg>';
// Build full swap focus HTML
let html = '<div class="swap-focus">';
html += `<div style="font-size:14px;font-weight:700;margin-bottom:8px;">🔍 交换细节 — <code style="font-size:15px;">${op}</code></div>`;
// Badges
if (removed.length > 0) {
html += `<span class="op-badge remove">✕ 删除:${removed.map(r => `${v(r.from)}→${v(r.to)}`).join(', ')}</span> `;
}
if (added.length > 0) {
html += `<span class="op-badge add">✓ 新增:${added.map(a => `${v(a.from)}→${v(a.to)}`).join(', ')}</span>`;
}
html += '<div class="ll-svg-wrap">' + svg + '</div>';
// Legend
html += '<div class="legend-row">';
html += '<span><span class="legend-dot normal"></span> 未改变</span>';
html += '<span><span class="legend-dot removed"></span> 删除的连接</span>';
html += '<span><span class="legend-dot added"></span> 新增的连接</span>';
html += '</div>';
html += '</div>';
return html;
}
// --- Pointer key legend ---
function renderPtrKey() {
return '<div class="ptr-key">' +
'<span><span class="ptr-dot c-prev"></span> prev</span>' +
'<span><span class="ptr-dot c-first"></span> first</span>' +
'<span><span class="ptr-dot c-second"></span> second</span>' +
'<span><span class="ptr-dot c-temp"></span> temp</span>' +
'</div>';
}
// ========== Main Render ==========
let simNodes, simNullId;
function render(stepIdx) {
const s = steps[stepIdx];
renderPipeline(s.stage);
// Main visualization
let vizHtml = renderPtrKey();
vizHtml += '<div class="ll-svg-wrap">' + renderListSVG(s.nextMap, simNodes, simNullId, s.ptrs, s.highlights, s.arrowOps) + '</div>';
// Swap focus (only for swap sub-steps)
if (s.swapFocus) {
vizHtml += renderSwapFocus(s.swapFocus, simNodes, simNullId);
}
$('vizArea').innerHTML = vizHtml;
// Detail panel
$('detailContent').innerHTML = '<div class="calc-block">' + s.detail.replace(/\n/g, '<br>') + '</div>';
// Result panel
if (s.isFinal && s.result) {
const resStr = s.result.length > 0 ? s.result.join(' → ') + ' → NULL' : 'NULL (空链表)';
$('resultContent').innerHTML = `<div class="final-answer">交换结果:<b>${resStr}</b><div class="complexity">时间复杂度 O(n) | 空间复杂度 O(1)</div></div>`;
} else if (s.stage === '完成' && !s.isFinal) {
// Intermediate "完成" step (loop exit, before final result)
// Don't overwrite result
}
// Hint
$('hintText').textContent = s.msg;
// Code highlight
$('codeArea').innerHTML = renderCode(CODE, { highlightLines: s.codeLines || [], lang: 'Python' });
}
// ========== Parse Input ==========
function parseInput() {
const raw = $('inputArea').value.trim();
const m = raw.match(/head\s*=\s*\[([^\]]*)\]/);
if (!m) {
alert('输入格式:head=[1,2,3,4]');
return null;
}
const inner = m[1].trim();
if (inner === '') return [];
return inner.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n));
}
// ========== Build & Init ==========
function buildAndStart(values) {
const sim = simulate(values);
steps = sim.stepsArr;
simNodes = sim.nodes;
simNullId = sim.nullId;
stepCtrl = new StepController({
onStep: (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx + 1} / ${steps.length}`; },
onComplete: () => {},
autoInterval: 800,
});
stepCtrl.setSteps(steps.map((_, i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
$('autoBtn').textContent = '自动播放';
}
function init() {
// Populate examples
const sel = $('exampleSelect');
examples.forEach((e, i) => {
sel.innerHTML += `<option value="${i}">${e.label}</option>`;
});
// Default start
buildAndStart(examples[0].input);
// Event handlers
$('applyBtn').onclick = () => {
const values = parseInput();
if (values !== null) buildAndStart(values);
};
sel.onchange = () => {
const e = examples[parseInt(sel.value)];
$('inputArea').value = `head=[${e.input.join(',')}]`;
buildAndStart(e.input);
};
$('prevBtn').onclick = () => stepCtrl.prev();
$('nextBtn').onclick = () => stepCtrl.next();
$('jumpBtn').onclick = () => stepCtrl.jumpToEnd();
$('autoBtn').onclick = () => {
const on = stepCtrl.toggleAuto();
$('autoBtn').textContent = on ? '暂停' : '自动播放';
};
$('resetBtn').onclick = () => {
stepCtrl.reset();
$('autoBtn').textContent = '自动播放';
};
}
init();
// Render code initially
$('codeArea').innerHTML = renderCode(CODE, { lang: 'Python' });
})();
</script>
</body>
</html>