feat: LeetCode Hot 100 - 100道题完整交互式图解页面
Deploy / deploy (push) Successful in 7s

This commit is contained in:
2026-08-24 04:35:13 +00:00
parent e4fe5afb80
commit 4f830ad352
114 changed files with 32137 additions and 82 deletions
+196
View File
@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>027. 合并两个有序链表 – 图解</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>🟢 027. 合并两个有序链表 <span class="badge easy">简单</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;">&#8617;</span>';
} else if (i < nodes.length - 1) {
html += '<span class="arrow">&rarr;</span>';
} else if (showNull && i === nodes.length - 1) {
html += '<span class="arrow" style="color:var(--text-muted);">&empty;</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 = [
{list1:[1,2,4], list2:[1,3,4], label:'示例1: [1,2,4]+[1,3,4]'},
{list1:[], list2:[], label:'示例2: 两空链表'},
{list1:[], list2:[0], label:'示例3: []+[0]'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var l1 = ex.list1.slice(), l2 = ex.list2.slice();
steps.push({stage:'init', msg:'双指针合并:比较两个链表头,取较小者加入结果', i1:0, i2:0, merged:[], phase:'merge'});
var i1 = 0, i2 = 0, merged = [];
while (i1 < l1.length && i2 < l2.length) {
steps.push({stage:'compare', msg:'比较 l1['+i1+']=' + l1[i1] + ' vs l2['+i2+']=' + l2[i2], i1:i1, i2:i2, merged:merged.slice(), phase:'merge'});
if (l1[i1] <= l2[i2]) {
merged.push(l1[i1]);
steps.push({stage:'take1', msg:'取 l1['+i1+']=' + l1[i1] + ' 加入结果', i1:i1+1, i2:i2, merged:merged.slice(), phase:'merge'});
i1++;
} else {
merged.push(l2[i2]);
steps.push({stage:'take2', msg:'取 l2['+i2+']=' + l2[i2] + ' 加入结果', i1:i1, i2:i2+1, merged:merged.slice(), phase:'merge'});
i2++;
}
}
while (i1 < l1.length) { merged.push(l1[i1]); i1++; }
while (i2 < l2.length) { merged.push(l2[i2]); i2++; }
steps.push({stage:'done', msg:'合并完成:[' + merged.join(',') + ']', i1:i1, i2:i2, merged:merged.slice(), phase:'done'});
}
function render(step) {
var s = steps[step];
var exIdx = parseInt($('exampleSelect').value) || 0;
var ex = examples[exIdx];
var hl1 = {}, hl2 = {};
if (s.i1 < ex.list1.length) {
hl1[s.i1] = s.stage === 'take1' ? 'active' : 'current';
}
if (s.i2 < ex.list2.length) {
hl2[s.i2] = s.stage === 'take2' ? 'active' : 'current';
}
var viz = llViz(ex.list1, {highs:hl1, ptrs:{i1:s.i1<ex.list1.length?s.i1:-1}, label:'链表 1'});
viz += llViz(ex.list2, {highs:hl2, ptrs:{i2:s.i2<ex.list2.length?s.i2:-1}, label:'链表 2'});
if (s.merged.length > 0) {
viz += llViz(s.merged, {highs:{[s.merged.length-1]:'active'}, label:'合并结果', showNull:s.phase==='done'?true: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.merged.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','compare\u2192比较','take1\u2192取L1','take2\u2192取L2','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = 'L1=[1,2,4] L2=[1,3,4]';
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 mergeTwoLists(l1, l2):\n dummy = ListNode(0)\n curr = dummy\n while l1 and l2:\n if l1.val <= l2.val:\n curr.next = l1\n l1 = l1.next\n else:\n curr.next = l2\n l2 = l2.next\n curr = curr.next\n curr.next = l1 or l2\n return dummy.next', {lang:'Python'});
})();
</script>
</body>
</html>