290 lines
12 KiB
HTML
290 lines
12 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>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>
|