223 lines
10 KiB
HTML
223 lines
10 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-Hans">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>032. 随机链表的复制 – 图解</title>
|
|||
|
|
<link rel="stylesheet" href="../shared/style.css">
|
|||
|
|
<style>
|
|||
|
|
/* page-specific overrides */
|
|||
|
|
.vis-area { min-height: 120px; padding: 16px 0; }
|
|||
|
|
.code-section { margin-top: 16px; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<h1>🟡 032. 随机链表的复制 <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" placeholder="默认示例,可自定义">
|
|||
|
|
<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() {
|
|||
|
|
// ========== Algorithm Logic ==========
|
|||
|
|
|
|||
|
|
function llViz(nodes, opts) {
|
|||
|
|
opts = opts || {};
|
|||
|
|
var highs = opts.highs || {};
|
|||
|
|
var ptrs = opts.ptrs || {};
|
|||
|
|
var cycle = opts.cycle != null ? opts.cycle : -1;
|
|||
|
|
var label = opts.label || '';
|
|||
|
|
var showNull = opts.showNull !== false;
|
|||
|
|
var html = '';
|
|||
|
|
if (label) html += '<div style="font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:4px;">' + label + '</div>';
|
|||
|
|
html += '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:4px 0;">';
|
|||
|
|
for (var i = 0; i < nodes.length; i++) {
|
|||
|
|
var cls = highs[i] || '';
|
|||
|
|
html += '<div style="position:relative;display:inline-flex;flex-direction:column;align-items:center;">';
|
|||
|
|
html += '<div class="ll-node ' + cls + '">';
|
|||
|
|
html += '<span class="val">' + nodes[i] + '</span>';
|
|||
|
|
if (i === cycle) {
|
|||
|
|
html += '<span class="arrow" style="color:var(--red);font-weight:bold;">↩</span>';
|
|||
|
|
} else if (i < nodes.length - 1) {
|
|||
|
|
html += '<span class="arrow">→</span>';
|
|||
|
|
} else if (showNull && i === nodes.length - 1) {
|
|||
|
|
html += '<span class="arrow" style="color:var(--text-muted);">∅</span>';
|
|||
|
|
}
|
|||
|
|
html += '</div>';
|
|||
|
|
var plabels = [];
|
|||
|
|
var pkeys = Object.keys(ptrs);
|
|||
|
|
for (var pi = 0; pi < pkeys.length; pi++) {
|
|||
|
|
if (ptrs[pkeys[pi]] === i) plabels.push(pkeys[pi]);
|
|||
|
|
}
|
|||
|
|
if (plabels.length) {
|
|||
|
|
html += '<div style="font-size:10px;font-weight:700;color:var(--blue);margin-top:2px;white-space:nowrap;">' + plabels.join(',') + '</div>';
|
|||
|
|
}
|
|||
|
|
html += '</div>';
|
|||
|
|
}
|
|||
|
|
html += '</div>';
|
|||
|
|
return html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setPipeline(stages, current) {
|
|||
|
|
$('pipeline').innerHTML = stages.map(function(st) {
|
|||
|
|
var parts = st.split('\u2192');
|
|||
|
|
return '<span class="pipe-step ' + (current===parts[0]?'active':'') + '">' + parts[1] + '</span>';
|
|||
|
|
}).join('<i>\u2192</i>');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var examples = [
|
|||
|
|
{nodes:[[7,null],[13,0],[11,4],[10,2],[1,0]], label:'示例1: 5个节点'},
|
|||
|
|
{nodes:[[1,1],[2,1]], label:'示例2: 2个节点'},
|
|||
|
|
{nodes:[[3,null],[3,0],[3,null]], label:'示例3: 3个节点'},
|
|||
|
|
];
|
|||
|
|
var steps, stepCtrl;
|
|||
|
|
|
|||
|
|
function buildSteps(ex) {
|
|||
|
|
steps = [];
|
|||
|
|
var n = ex.nodes.length;
|
|||
|
|
steps.push({stage:'init', msg:'交错复制法:①在每个原节点后插入复制节点 ②设置random指针 ③拆分', phase:'interleave'});
|
|||
|
|
// Phase 1: interleave
|
|||
|
|
var interleaved = [];
|
|||
|
|
for (var i = 0; i < n; i++) {
|
|||
|
|
interleaved.push({val:ex.nodes[i][0], isCopy:false, origIdx:i});
|
|||
|
|
interleaved.push({val:ex.nodes[i][0], isCopy:true, origIdx:i});
|
|||
|
|
}
|
|||
|
|
steps.push({stage:'interleave', msg:'Phase1:在每个原节点后插入其复制 → ' + interleaved.map(function(x){return x.val+(x.isCopy?"'":"");}).join(','), phase:'interleave', interleaved:interleaved.slice()});
|
|||
|
|
// Phase 2: set random pointers
|
|||
|
|
var randomLinks = [];
|
|||
|
|
for (var i = 0; i < n; i++) {
|
|||
|
|
var rnd = ex.nodes[i][1];
|
|||
|
|
if (rnd !== null) {
|
|||
|
|
randomLinks.push({from:i*2+1, to:rnd*2+1, msg:"copy["+i+"].random = copy["+rnd+"] (值="+ex.nodes[rnd][0]+")"});
|
|||
|
|
} else {
|
|||
|
|
randomLinks.push({from:i*2+1, to:-1, msg:"copy["+i+"].random = null"});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
steps.push({stage:'random', msg:'Phase2:设置random指针 → ' + randomLinks.map(function(r){return r.msg;}).join('; '), phase:'random', interleaved:interleaved.slice(), randomLinks:randomLinks});
|
|||
|
|
// Phase 3: split
|
|||
|
|
var original = [], copy = [];
|
|||
|
|
for (var i = 0; i < interleaved.length; i++) {
|
|||
|
|
if (i%2===0) original.push(interleaved[i].val);
|
|||
|
|
else copy.push(interleaved[i].val);
|
|||
|
|
}
|
|||
|
|
steps.push({stage:'split', msg:'Phase3:拆分为原链表和复制链表', phase:'split', original:original, copy:copy});
|
|||
|
|
steps.push({stage:'done', msg:'深拷贝完成', phase:'done', copy:copy});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderInterleaved(interleaved, hlIdx) {
|
|||
|
|
var html = '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:8px 0;">';
|
|||
|
|
for (var i = 0; i < interleaved.length; i++) {
|
|||
|
|
var n = interleaved[i];
|
|||
|
|
var bg = n.isCopy ? 'var(--green-light)' : 'var(--blue-light)';
|
|||
|
|
var border = n.isCopy ? 'var(--green)' : 'var(--blue)';
|
|||
|
|
var label = n.val + (n.isCopy ? "'" : '');
|
|||
|
|
var hl = (hlIdx === i) ? 'box-shadow:0 0 0 3px var(--orange);' : '';
|
|||
|
|
html += '<div style="display:inline-flex;flex-direction:column;align-items:center;">';
|
|||
|
|
html += '<div style="min-width:40px;height:36px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;padding:0 8px;border:2px solid '+border+';border-radius:8px;background:'+bg+';'+hl+';">'+label+'</div>';
|
|||
|
|
html += '<div style="font-size:9px;color:var(--text-muted);">'+(n.isCopy?'copy':'orig')+'</div>';
|
|||
|
|
html += '</div>';
|
|||
|
|
if (i < interleaved.length - 1) html += '<span style="color:var(--text-muted);">→</span>';
|
|||
|
|
}
|
|||
|
|
html += '</div>';
|
|||
|
|
return html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function render(step) {
|
|||
|
|
var s = steps[step];
|
|||
|
|
var viz = '';
|
|||
|
|
if (s.phase === 'interleave' && s.interleaved) {
|
|||
|
|
viz = renderInterleaved(s.interleaved, -1);
|
|||
|
|
} else if (s.phase === 'random' && s.interleaved) {
|
|||
|
|
viz = renderInterleaved(s.interleaved, -1);
|
|||
|
|
viz += '<div style="margin-top:8px;"><b>Random 指针设置:</b></div>';
|
|||
|
|
s.randomLinks.forEach(function(r) {
|
|||
|
|
viz += '<div style="font-size:13px;margin:2px 0;">' + r.msg + '</div>';
|
|||
|
|
});
|
|||
|
|
} else if (s.phase === 'split' || s.phase === 'done') {
|
|||
|
|
viz = llViz(s.original, {highlights:{}, label:'原链表', showNull:false});
|
|||
|
|
viz += llViz(s.copy, {highlights:{}, label:'复制链表', showNull:false});
|
|||
|
|
}
|
|||
|
|
$('vizArea').innerHTML = viz;
|
|||
|
|
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
|
|||
|
|
if (s.stage === 'done') {
|
|||
|
|
$('resultContent').innerHTML = '<div class="final-answer">深拷贝链表:<b>[' + s.copy.join(' → ') + ']</b></div>';
|
|||
|
|
}
|
|||
|
|
$('hintText').textContent = s.msg;
|
|||
|
|
setPipeline(['init\u2192初始化','interleave\u2192交错插入','random\u2192设置随机','split\u2192拆分','done\u2192完成'], s.stage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function init() {
|
|||
|
|
var sel = $('exampleSelect');
|
|||
|
|
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
|
|||
|
|
$('inputArea').value = '[[7,null],[13,0],[11,4],[10,2],[1,0]]';
|
|||
|
|
buildSteps(examples[0]);
|
|||
|
|
stepCtrl = new StepController({onStep:render});
|
|||
|
|
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
|
|||
|
|
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
|
|||
|
|
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
|
|||
|
|
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
|
|||
|
|
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
|
|||
|
|
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
|
|||
|
|
$('nextBtn').onclick = function(){ stepCtrl.next(); };
|
|||
|
|
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
|
|||
|
|
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
|
|||
|
|
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
|
|||
|
|
}
|
|||
|
|
init();
|
|||
|
|
$('codeArea').innerHTML = renderCode('def copyRandomList(head):\n if not head: return None\n # 1. interleave copies\n cur = head\n while cur:\n copy = Node(cur.val, cur.next, None)\n cur.next = copy\n cur = copy.next\n # 2. set random pointers\n cur = head\n while cur:\n if cur.random:\n cur.next.random = cur.random.next\n cur = cur.next.next\n # 3. split\n cur = head\n copy_head = head.next\n while cur:\n copy = cur.next\n cur.next = copy.next\n if copy.next:\n copy.next = copy.next.next\n cur = cur.next\n return copy_head', {lang:'Python'});
|
|||
|
|
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|