Files
illustrated-algorithm/top-k-frequent-elements/index.html
T

290 lines
12 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>075. 前 K 个高频元素 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.freq-table { border-collapse:collapse; margin:8px 0; }
.freq-table th, .freq-table td { border:1px solid var(--border); padding:4px 12px; text-align:center; font-size:14px; }
.freq-table th { background:#f8fafc; color:var(--text-secondary); font-weight:600; }
.freq-table td.active-cell { background:#fef3c7; color:#92400e; font-weight:700; }
.freq-table td.in-heap { background:#dbeafe; color:#1e40af; }
.freq-table td.ejected { background:#fee2e2; color:#991b1b; opacity:0.5; text-decoration:line-through; }
.freq-table td.accepted { background:#dcfce7; color:#166534; font-weight:700; }
.heap-row { display:flex; justify-content:center; gap:6px; margin:6px 0; flex-wrap:wrap; }
.heap-cell {
min-width:48px; height:32px; display:inline-flex; align-items:center; justify-content:center;
border-radius:8px; font-weight:700; font-size:13px; font-family:monospace;
transition: all 0.25s ease; padding:2px 8px;
}
.heap-cell.in-heap { background:#dbeafe; color:#1e40af; border:1px solid #3b82f6; }
.heap-cell.top { background:#fef3c7; color:#92400e; border:2px solid #f59e0b; box-shadow:0 0 0 3px rgba(245,158,11,0.2); }
.heap-cell.new { background:#dcfce7; color:#166534; border:2px solid #16a34a; }
</style>
</head>
<body>
<div class="container">
<h1>🟡 075. 前 K 个高频元素 <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" value="nums=[1,1,1,2,2,3], k=2" placeholder="nums=[...], k=N">
<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() {
const examples = [
{nums:[1,1,1,2,2,3], k:2, label:'示例1: [1,1,1,2,2,3], k=2'},
{nums:[1], k:1, label:'示例2: [1], k=1'},
{nums:[4,1,-1,2,-1,2,3], k:2, label:'示例3: [4,1,-1,2,-1,2,3], k=2'},
];
let nums, k, steps, stepCtrl;
function heapPush(heap, item) {
// item = [freq, val], min-heap by freq
heap.push(item);
let i = heap.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (heap[p][0] > heap[i][0]) { [heap[p], heap[i]] = [heap[i], heap[p]]; i = p; }
else break;
}
}
function heapPop(heap) {
const top = heap[0];
const last = heap.pop();
if (heap.length) {
heap[0] = last;
let i = 0;
while (true) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < heap.length && heap[l][0] < heap[s][0]) s = l;
if (r < heap.length && heap[r][0] < heap[s][0]) s = r;
if (s !== i) { [heap[i], heap[s]] = [heap[s], heap[i]]; i = s; }
else break;
}
}
return top;
}
function buildSteps(arr, kVal) {
nums = arr.slice(); k = kVal;
steps = [];
// Step 1: Count frequencies
const freq = {};
arr.forEach(n => freq[n] = (freq[n] || 0) + 1);
const entries = Object.entries(freq).map(([v,f]) => [parseInt(v), f]).sort((a,b) => b[1] - a[1]);
steps.push({stage:'start', msg:`开始统计每个元素出现频率,共 ${entries.length} 种不同元素`, freq, entries, heap:[], currentVal:null, ejected:null, heapSet:new Set()});
steps.push({stage:'count', msg:`频率统计完成!共 ${entries.length} 种元素,需取前 ${k} 高频`, freq, entries, heap:[], currentVal:null, ejected:null, heapSet:new Set()});
// Step 2: Build initial min-heap with first k entries
const heap = [];
const heapSet = new Set();
for (let i = 0; i < Math.min(kVal, entries.length); i++) {
const [val, cnt] = entries[i];
heapPush(heap, [cnt, val]);
heapSet.add(val);
steps.push({stage:'build', msg:`将元素 ${val}(频率=${cnt}) 加入小顶堆`, freq, entries, heap:heap.map(h=>h.slice()), currentVal:val, ejected:null, heapSet:new Set(heapSet)});
}
if (entries.length <= kVal) {
steps.push({stage:'done', msg:`元素种类 ≤ k,全部入选。结果:[${entries.map(e=>e[0]).join(',')}]`, freq, entries, heap:heap.map(h=>h.slice()), currentVal:null, ejected:null, heapSet:new Set(heapSet), result:entries.map(e=>e[0])});
return;
}
// Step 3: Process remaining entries (already sorted by freq desc, so they all have lower freq)
for (let i = kVal; i < entries.length; i++) {
const [val, cnt] = entries[i];
const topFreq = heap[0][0];
steps.push({stage:'compare', msg:`检查元素 ${val}(频率=${cnt}),堆顶频率=${topFreq}`, freq, entries, heap:heap.map(h=>h.slice()), currentVal:val, ejected:null, heapSet:new Set(heapSet)});
if (cnt > topFreq) {
const ejected = heapPop(heap);
heapSet.delete(ejected[1]);
heapPush(heap, [cnt, val]);
heapSet.add(val);
steps.push({stage:'replace', msg:`${cnt} > ${topFreq},弹出 ${ejected[1]}(freq=${ejected[0]}),压入 ${val}(freq=${cnt})`, freq, entries, heap:heap.map(h=>h.slice()), currentVal:val, ejected:ejected[1], heapSet:new Set(heapSet)});
} else {
steps.push({stage:'skip', msg:`${cnt} ≤ ${topFreq},跳过`, freq, entries, heap:heap.map(h=>h.slice()), currentVal:val, ejected:null, heapSet:new Set(heapSet)});
}
}
const result = heap.map(h => h[1]);
steps.push({stage:'done', msg:`遍历结束,堆中 ${k} 个元素即为前 K 高频`, freq, entries, heap:heap.map(h=>h.slice()), currentVal:null, ejected:null, heapSet:new Set(heapSet), result});
}
function renderFreqTable(freq, entries, heapSet, currentVal, ejected) {
let html = '<table class="freq-table"><tr><th>元素</th>';
entries.forEach(([val]) => {
let cls = '';
if (val === currentVal) cls = 'active-cell';
else if (ejected !== null && val === ejected) cls = 'ejected';
else if (heapSet.has(val)) cls = 'in-heap';
html += `<td class="${cls}">${val}</td>`;
});
html += '</tr><tr><th>频率</th>';
entries.forEach(([val, cnt]) => {
let cls = '';
if (val === currentVal) cls = 'active-cell';
else if (ejected !== null && val === ejected) cls = 'ejected';
else if (heapSet.has(val)) cls = 'in-heap';
html += `<td class="${cls}">${cnt}</td>`;
});
html += '</tr></table>';
return html;
}
function renderHeapVis(heap) {
if (!heap || heap.length === 0) return '<div style="color:#94a3b8;font-style:italic;">空堆</div>';
let html = '<div style="text-align:center;padding:8px 0;"><b>小顶堆 (按频率)</b><br>';
html += '<div class="heap-row">';
heap.forEach((h, i) => {
const cls = i === 0 ? 'top' : 'in-heap';
html += `<span class="heap-cell ${cls}">${h[1]}:${h[0]}</span>`;
});
html += '</div></div>';
return html;
}
function render(step) {
const s = steps[step];
let viz = '<div style="margin-bottom:8px;"><b>频率统计表:</b></div>';
viz += renderFreqTable(s.freq, s.entries, s.heapSet, s.currentVal, s.ejected);
if (s.heap && s.heap.length > 0) {
viz += '<div style="margin:12px 0;">' + renderHeapVis(s.heap) + '</div>';
viz += `<div style="font-size:13px;color:#64748b;">堆顶: 元素${s.heap[0][1]}, 频率=${s.heap[0][0]}</div>`;
}
$('vizArea').innerHTML = viz;
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (s.heap && s.heap.length > 0) {
detail += '<div style="margin-top:8px;font-size:13px;">当前堆内容: ' + s.heap.map(h=>`${h[1]}(×${h[0]})`).join(', ') + '</div>';
}
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
$('resultContent').innerHTML = `<div class="final-answer">前 <b>${k}</b> 个高频元素: <b>[${s.result.join(', ')}]</b><br><br>思路:① 统计频率 ② 小顶堆维护前 K 高频<br>时间 O(n log k),空间 O(n)</div>`;
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','count→统计频率','build→建堆','compare→比较','replace→替换','skip→跳过','done→完成'];
$('pipeline').innerHTML = stages.map(st => {
const [key, label] = st.split('→');
return `<span class="pipe-step ${s.stage===key?'active':''}">${label}</span>`;
}).join('<i>→</i>');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
buildSteps(examples[0].nums, examples[0].k);
stepCtrl = new StepController({onStep: render});
stepCtrl.setSteps(steps.map((_,i) => i));
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx+1} / ${steps.length}`; };
$('applyBtn').onclick = () => {
const m = $('inputArea').value.match(/nums=\[([^\]]+)\]\s*,\s*k=(\d+)/);
if (!m) { alert('格式: nums=[1,1,1,2,2,3], k=2'); return; }
const arr = m[1].split(',').map(Number);
const kVal = parseInt(m[2]);
buildSteps(arr, kVal);
stepCtrl.setSteps(steps.map((_,i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = `nums=[${e.nums}], k=${e.k}`;
buildSteps(e.nums, e.k);
stepCtrl.setSteps(steps.map((_,i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
};
$('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(`import heapq
from collections import Counter
def topKFrequent(nums, k):
# 1. 统计频率
freq = Counter(nums)
# 2. 小顶堆维护前 k 高频
heap = []
for val, cnt in freq.items():
if len(heap) < k:
heapq.heappush(heap, (cnt, val))
elif cnt > heap[0][0]:
heapq.heapreplace(heap, (cnt, val))
# 3. 返回堆中元素
return [val for cnt, val in heap]
# 复杂度:O(n log k) 时间,O(n) 空间`, {lang:'Python'});
})();
</script>
</body>
</html>