200 lines
9.2 KiB
HTML
200 lines
9.2 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>024. 回文链表 – 图解</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>🟢 024. 回文链表 <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;">↩</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 = [
|
||
{input:[1,2,2,1], label:'示例1: [1,2,2,1] 回文'},
|
||
{input:[1,2], label:'示例2: [1,2] 非回文'},
|
||
{input:[1,2,3,2,1], label:'示例3: [1,2,3,2,1] 回文'},
|
||
];
|
||
var nodes, steps, stepCtrl;
|
||
|
||
function buildSteps(arr) {
|
||
nodes = arr.slice(); steps = [];
|
||
if (arr.length === 0) { steps.push({stage:'done',msg:'空链表是回文',phase:'done'}); return; }
|
||
steps.push({stage:'init', msg:'快慢指针找中点 + 反转后半部分 + 比较', phase:'find_mid', slow:0, fast:0});
|
||
// Phase 1: find middle
|
||
var slow = 0, fast = 0;
|
||
while (fast < arr.length - 1 && fast + 1 < arr.length - 1) {
|
||
slow++; fast += 2;
|
||
steps.push({stage:'move', msg:'slow→'+arr[slow]+', fast→'+arr[Math.min(fast,arr.length-1)], phase:'find_mid', slow:slow, fast:Math.min(fast,arr.length-1)});
|
||
}
|
||
var mid = slow + 1;
|
||
steps.push({stage:'mid', msg:'中点后半起始位置:index ' + mid + ' (值=' + arr[mid] + ')', phase:'find_mid', slow:slow, fast:Math.min(fast,arr.length-1), mid:mid});
|
||
// Phase 2: reverse second half
|
||
var second = arr.slice(mid);
|
||
var reversed = second.slice().reverse();
|
||
steps.push({stage:'reverse', msg:'反转后半 [' + second.join(',') + '] → [' + reversed.join(',') + ']', phase:'reverse', second:second.slice(), reversed:reversed.slice()});
|
||
// Phase 3: compare
|
||
var first = arr.slice(0, arr.length - second.length);
|
||
var isPalin = true;
|
||
for (var i = 0; i < reversed.length; i++) {
|
||
if (first[i] !== reversed[i]) { isPalin = false; break; }
|
||
}
|
||
steps.push({stage:'compare', msg:'前半 ['+first.join(',')+'] vs 反转后半 ['+reversed.join(',')+'] → ' + (isPalin?'匹配 ✓':'不匹配 ✗'), phase:'compare', first:first, reversed:reversed, isPalin:isPalin});
|
||
steps.push({stage:'done', msg:'结果:' + (isPalin?'是回文链表':'不是回文链表'), phase:'done', isPalin:isPalin});
|
||
}
|
||
|
||
function render(step) {
|
||
var s = steps[step];
|
||
var viz = '';
|
||
if (s.phase === 'find_mid' || s.stage === 'init') {
|
||
var hl = {};
|
||
if (s.slow >= 0 && s.slow < nodes.length) hl[s.slow] = 'active';
|
||
if (s.fast >= 0 && s.fast < nodes.length) hl[s.fast] = 'current';
|
||
viz = llViz(nodes, {highs:hl, ptrs:{slow:s.slow, fast:s.fast}, label:'寻找中点'});
|
||
} else if (s.phase === 'reverse') {
|
||
viz = llViz(nodes, {highs:{}, label:'原始链表'});
|
||
viz += llViz(s.reversed, {highs:{}, label:'反转后半', showNull:false});
|
||
} else if (s.phase === 'compare' || s.phase === 'done') {
|
||
viz = llViz(s.first, {label:'前半部分', showNull:false});
|
||
viz += llViz(s.reversed, {label:'反转后半', showNull:false});
|
||
viz += '<div style="margin-top:8px;font-size:15px;">' + (s.isPalin ? '<span style="color:var(--green);font-weight:700;">✓ 匹配</span>' : '<span style="color:var(--red);font-weight:700;">✗ 不匹配</span>') + '</div>';
|
||
}
|
||
$('vizArea').innerHTML = viz;
|
||
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
|
||
if (s.stage === 'done') {
|
||
$('resultContent').innerHTML = '<div class="final-answer">' + (s.isPalin?'是回文链表 ✓':'不是回文链表 ✗') + '</div>';
|
||
}
|
||
$('hintText').textContent = s.msg;
|
||
setPipeline(['init\u2192开始','move\u2192快慢','mid\u2192中点','reverse\u2192反转','compare\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 = '[1,2,2,1]';
|
||
buildSteps(examples[0].input);
|
||
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(){ try { var arr=JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); } catch(e){ alert('请输入合法 JSON 数组'); } };
|
||
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)].input); 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 isPalindrome(head):\n slow = fast = head\n while fast and fast.next:\n slow = slow.next\n fast = fast.next.next\n prev = None\n while slow:\n nxt = slow.next\n slow.next = prev\n prev = slow\n slow = nxt\n left, right = head, prev\n while right:\n if left.val != right.val:\n return False\n left = left.next\n right = right.next\n return True', {lang:'Python'});
|
||
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html> |