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
+258
View File
@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>035. LRU 缓存 – 图解</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>🟡 035. LRU 缓存 <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;">&#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 = [
{ops:['LRUCache(2)','put(1,1)','put(2,2)','get(1)','put(3,3)','get(2)','put(4,4)','get(1)','get(3)','get(4)'], label:'示例1: capacity=2'},
{ops:['LRUCache(3)','put(1,1)','put(2,2)','put(3,3)','get(1)','put(4,4)','get(2)'], label:'示例2: capacity=3'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var cap = 2;
var ops = ex.ops;
var capMatch = ops[0].match(/LRUCache\((\d+)\)/);
if (capMatch) cap = parseInt(capMatch[1]);
// DLL: head <-> ... <-> tail (head=most recent, tail=least recent)
var dll = []; // [{key, val}]
var map = {}; // key -> index in dll
var capacity = cap;
steps.push({stage:'init', msg:'LRU 缓存初始化,capacity=' + cap + '。双向链表(头=最近,尾=最久)+ 哈希表', dll:[], map:{}, capacity:capacity, op:ops[0]});
for (var opIdx = 1; opIdx < ops.length; opIdx++) {
var op = ops[opIdx];
var getMatch = op.match(/get\((\d+)\)/);
var putMatch = op.match(/put\((\d+),(\d+)\)/);
if (getMatch) {
var key = getMatch[1];
if (key in map) {
// hit: move to front
var idx = map[key];
var node = dll[idx];
dll.splice(idx, 1);
dll.unshift(node);
// rebuild map
map = {};
for (var mi = 0; mi < dll.length; mi++) map[dll[mi].key] = mi;
steps.push({stage:'hit', msg:'get('+key+') 命中!返回 '+node.val+',移到链表头部', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op, hitVal:node.val});
} else {
steps.push({stage:'miss', msg:'get('+key+') 未命中,返回 -1', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op, hitVal:-1});
}
} else if (putMatch) {
var key = putMatch[1];
var val = putMatch[2];
if (key in map) {
// update: move to front
var idx = map[key];
dll[idx].val = val;
var node = dll[idx];
dll.splice(idx, 1);
dll.unshift(node);
map = {};
for (var mi = 0; mi < dll.length; mi++) map[dll[mi].key] = mi;
steps.push({stage:'update', msg:'put('+key+','+val+') 更新已有key,移到头部', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op});
} else {
if (dll.length >= capacity) {
// evict LRU (tail)
var evicted = dll.pop();
delete map[evicted.key];
steps.push({stage:'evict', msg:'put('+key+','+val+') 容量已满,淘汰尾部 key='+evicted.key+' (val='+evicted.val+')', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op, evicted:evicted.key});
}
dll.unshift({key:key, val:val});
map = {};
for (var mi = 0; mi < dll.length; mi++) map[dll[mi].key] = mi;
steps.push({stage:'insert', msg:'put('+key+','+val+') 插入到头部', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op});
}
}
}
steps.push({stage:'done', msg:'操作完毕', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity});
}
function renderDLL(dll) {
if (!dll || dll.length === 0) return '<div style="color:var(--text-muted);font-style:italic;padding:8px 0;">[空]</div>';
var html = '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:2px;padding:4px 0;">';
html += '<div style="padding:2px 6px;font-size:11px;font-weight:700;color:var(--green);">HEAD</div>';
for (var i = 0; i < dll.length; i++) {
var n = dll[i];
var isHead = (i === 0);
var isTail = (i === dll.length - 1);
var bg = isHead ? 'var(--green-light)' : isTail ? 'var(--orange-light)' : 'var(--blue-light)';
var border = isHead ? 'var(--green)' : isTail ? 'var(--orange)' : 'var(--blue)';
html += '<div style="border:2px solid '+border+';border-radius:8px;overflow:hidden;display:inline-flex;">';
html += '<div style="min-width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;padding:0 6px;background:'+bg+';color:var(--text);">k'+n.key+'</div>';
html += '<div style="min-width:24px;height:32px;display:flex;align-items:center;justify-content:center;font-size:12px;padding:0 4px;background:#f8fafc;border-left:1px solid var(--border);">v'+n.val+'</div>';
html += '</div>';
if (i < dll.length - 1) html += '<span style="color:var(--text-muted);">&harr;</span>';
}
html += '<div style="padding:2px 6px;font-size:11px;font-weight:700;color:var(--orange);">TAIL</div>';
html += '</div>';
return html;
}
function render(step) {
var s = steps[step];
var viz = '<div style="margin-bottom:6px;font-size:14px;font-weight:600;color:var(--text-secondary);">当前操作:<code>' + s.op + '</code></div>';
viz += renderDLL(s.dll);
// Show map
if (s.map && Object.keys(s.map).length > 0) {
viz += '<div style="margin-top:8px;font-size:13px;"><b>HashMap:</b>';
Object.keys(s.map).forEach(function(k) {
viz += '<code>'+k+'→idx'+s.map[k]+'</code> ';
});
viz += '</div>';
}
viz += '<div style="margin-top:6px;font-size:12px;color:var(--text-muted);">容量: ' + s.dll.length + '/' + s.capacity + '</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.hitVal !== undefined) {
$('detailContent').innerHTML += '<div class="current-answer">返回值:<b>' + s.hitVal + '</b></div>';
}
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">缓存最终状态:[' + s.dll.map(function(n){return n.key+':'+n.val;}).join(', ') + ']</div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','hit\u2192命中','miss\u2192未命中','update\u2192更新','evict\u2192淘汰','insert\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 = 'LRUCache(2) put(1,1) put(2,2) get(1) put(3,3) get(2)';
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('class LRUCache:\n def __init__(self, capacity):\n self.cap = capacity\n self.cache = {}\n self.head = DLinkedNode()\n self.tail = DLinkedNode()\n self.head.next = self.tail\n self.tail.prev = self.head\n\n def _remove(self, node):\n node.prev.next = node.next\n node.next.prev = node.prev\n\n def _add_to_head(self, node):\n node.prev = self.head\n node.next = self.head.next\n self.head.next.prev = node\n self.head.next = node\n\n def get(self, key):\n if key not in self.cache:\n return -1\n node = self.cache[key]\n self._remove(node)\n self._add_to_head(node)\n return node.val\n\n def put(self, key, value):\n if key in self.cache:\n node = self.cache[key]\n node.val = value\n self._remove(node)\n self._add_to_head(node)\n else:\n if len(self.cache) >= self.cap:\n tail = self.tail.prev\n self._remove(tail)\n del self.cache[tail.key]\n node = DLinkedNode(key, value)\n self.cache[key] = node\n self._add_to_head(node)', {lang:'Python'});
})();
</script>
</body>
</html>