Files
illustrated-algorithm/lru-cache/index.html
T
2026-08-24 04:35:13 +00:00

258 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>