Files
2026-08-24 04:35:13 +00:00

241 lines
11 KiB
HTML
Raw Permalink 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>054. 实现 Trie (前缀树) – 图解</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>🟡 054. 实现 Trie (前缀树) <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 ==========
const examples = [
{ops: ['insert("apple")','search("apple")','search("app")','startsWith("app")','insert("app")','search("app")'], label: '示例1: apple/app'},
{ops: ['insert("dog")','insert("deer")','search("dog")','startsWith("de")'], label: '示例2: dog/deer'},
];
let steps, stepCtrl, trieNodes;
function buildSteps(ops) {
steps = [];
trieNodes = [{id:0, char:'ROOT', children:{}, isEnd:false, depth:0}];
steps.push({stage:'init', msg:'初始化Trie根节点', path:[], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'init'});
for (const op of ops) {
const insertM = op.match(/insert\("(\w+)"\)/);
const searchM = op.match(/search\("(\w+)"\)/);
const startsM = op.match(/startsWith\("(\w+)"\)/);
if (insertM) {
const word = insertM[1];
steps.push({stage:'op_start', msg:`操作: insert("${word}")`, path:[], currentNode:0, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
let cur = 0;
const path = [0];
for (let i=0; i<word.length; i++) {
const ch = word[i];
if (!trieNodes[cur].children[ch]) {
const newId = trieNodes.length;
trieNodes.push({id:newId, char:ch, children:{}, isEnd:false, depth:i+1});
trieNodes[cur].children[ch] = newId;
steps.push({stage:'create', msg:`创建新节点 '${ch}'(不存在)`, path:[...path], currentNode:newId, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
} else {
steps.push({stage:'traverse', msg:`节点 '${ch}' 已存在,沿路径前进`, path:[...path], currentNode:trieNodes[cur].children[ch], nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
}
cur = trieNodes[cur].children[ch];
path.push(cur);
}
trieNodes[cur].isEnd = true;
steps.push({stage:'mark_end', msg:`标记节点为单词结尾 (${word})`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
} else if (searchM) {
const word = searchM[1];
steps.push({stage:'op_start', msg:`操作: search("${word}")`, path:[], currentNode:0, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
let cur = 0; let found = true;
const path = [0];
for (let i=0; i<word.length; i++) {
const ch = word[i];
if (!trieNodes[cur].children[ch]) {
found = false;
steps.push({stage:'not_found', msg:`节点 '${ch}' 不存在,搜索失败`, path:[...path], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
break;
}
cur = trieNodes[cur].children[ch];
path.push(cur);
steps.push({stage:'traverse', msg:`沿 '${ch}' 前进`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
}
if (found) {
if (trieNodes[cur].isEnd) steps.push({stage:'found', msg:`"${word}" 存在且为完整单词 ✓`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
else steps.push({stage:'prefix_only', msg:`"${word}" 只是前缀,不是完整单词`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
}
} else if (startsM) {
const prefix = startsM[1];
steps.push({stage:'op_start', msg:`操作: startsWith("${prefix}")`, path:[], currentNode:0, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
let cur = 0; let found = true;
const path = [0];
for (let i=0; i<prefix.length; i++) {
const ch = prefix[i];
if (!trieNodes[cur].children[ch]) {
found = false;
steps.push({stage:'not_found', msg:`节点 '${ch}' 不存在,前缀不存在`, path:[...path], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
break;
}
cur = trieNodes[cur].children[ch];
path.push(cur);
steps.push({stage:'traverse', msg:`沿 '${ch}' 前进`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
}
if (found) steps.push({stage:'found', msg:`前缀 "${prefix}" 存在 ✓`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
}
}
steps.push({stage:'done', msg:'所有操作完成', path:[], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'done'});
}
function renderTrie(nodes, path, current) {
const pathSet = new Set(path);
// Build adjacency from nodes
function buildTree(nodeId) {
const node = nodes[nodeId];
const childKeys = Object.keys(node.children);
if (childKeys.length === 0) return `<div class="tree-node"><div class="node-circle ${pathSet.has(nodeId)?(nodeId===current?'current':'visited'):''}${node.isEnd?' selected':''}">${node.char==='ROOT'?'⊘':node.char}</div>${node.isEnd?'<span style="font-size:9px;color:var(--green);">●</span>':''}</div>`;
const children = childKeys.map(k => buildTree(node.children[k]));
return `<div class="tree-node"><div class="node-circle ${pathSet.has(nodeId)?(nodeId===current?'current':'visited'):''}${node.isEnd?' selected':''}">${node.char==='ROOT'?'⊘':node.char}</div>${node.isEnd?'<span style="font-size:9px;color:var(--green);">●</span>':''}<div class="tree-children">${children.join('')}</div></div>`;
}
return '<div class="tree-container">' + buildTree(0) + '</div>';
}
function render(step) {
const s = steps[step];
let viz = renderTrie(s.nodes, s.path, s.currentNode);
viz += `<div style="margin-top:8px;"><b>当前操作:</b><code>${s.op}</code></div>`;
viz += '<div style="margin-top:4px;display:flex;gap:12px;font-size:13px;"><span style="color:var(--green);">● 单词结尾</span><span style="color:#fef3c7;border:2px solid #f59e0b;border-radius:50%;width:14px;height:14px;display:inline-block;"></span> 当前路径</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') {
$('resultContent').innerHTML = '<div class="final-answer">所有操作完成 ✓</div>';
}
$('hintText').textContent = s.msg;
const stages = ['init→初始化','op_start→开始操作','create→创建节点','traverse→遍历','mark_end→标记结尾','found→找到','not_found→未找到','prefix_only→仅前缀','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `<span class="pipe-step ${s.stage===k?'active':''}">${l}</span>`; }).join('<i>→</i>');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
buildSteps(examples[0].ops);
stepCtrl = new StepController({onStep: render, autoInterval:700});
stepCtrl.setSteps(steps.map((_,i)=>i));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx+1} / ${steps.length}`; };
$('applyBtn').onclick = () => {
try {
const ops = JSON.parse($('inputArea').value);
buildSteps(ops); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
} catch(e) { alert('请输入操作数组,如 ["insert(\\"apple\\")","search(\\"app\\")"]'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.ops); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
};
$('prevBtn').onclick = () => stepCtrl.prev();
$('nextBtn').onclick = () => stepCtrl.next();
$('jumpBtn').onclick = () => stepCtrl.jumpToEnd();
$('autoBtn').onclick = () => { const on = stepCtrl.toggleAuto(); $('autoBtn').textContent = on ? '暂停' : '自动播放'; };
$('resetBtn').onclick = () => { stepCtrl.reset(); $('autoBtn').textContent = '自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode(`class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
def search(self, word):
node = self._find(word)
return node is not None and node.is_end
def startsWith(self, prefix):
return self._find(prefix) is not None
def _find(self, prefix):
node = self.root
for ch in prefix:
if ch not in node.children:
return None
node = node.children[ch]
return node`, {lang:'Python'});
})();
</script>
</body>
</html>