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
+334
View File
@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>076. 数据流的中位数 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.dual-heap { display:flex; gap:20px; flex-wrap:wrap; justify-content:center; margin:12px 0; }
.heap-col { flex:1; min-width:200px; max-width:400px; text-align:center; }
.heap-col h4 { margin:0 0 8px; font-size:14px; }
.heap-col.max-heap h4 { color:#1e40af; }
.heap-col.min-heap h4 { color:#991b1b; }
.heap-items { display:flex; flex-direction:column-reverse; align-items:center; gap:3px; min-height:40px; padding:8px; background:#f8fafc; border:2px dashed var(--border); border-radius:10px; }
.heap-col.max-heap .heap-items { border-color:#3b82f6; }
.heap-col.min-heap .heap-items { border-color:#ef4444; }
.h-item {
padding:4px 14px; border-radius:6px; font-weight:700; font-size:14px; font-family:monospace;
}
.h-item.max { background:#dbeafe; color:#1e40af; border:1px solid #3b82f6; }
.h-item.min { background:#fee2e2; color:#991b1b; border:1px solid #ef4444; }
.h-item.top { box-shadow:0 0 0 3px rgba(59,130,246,0.3); }
.h-item.top-min { box-shadow:0 0 0 3px rgba(239,68,68,0.3); }
.h-item.incoming { background:#fef3c7; color:#92400e; border:2px solid #f59e0b; }
.median-box {
background:#ecfdf5; border:2px solid #86efac; border-radius:12px;
padding:12px 20px; text-align:center; font-size:18px; margin-top:12px; font-weight:700;
}
.median-box .label { font-size:12px; color:#475569; font-weight:400; margin-bottom:4px; }
.stream-flow { display:flex; gap:4px; flex-wrap:wrap; align-items:center; margin:8px 0; }
.stream-num { padding:4px 10px; border-radius:6px; font-weight:600; font-size:14px; font-family:monospace; }
.stream-num.pending { background:#f1f5f9; color:#94a3b8; }
.stream-num.added { background:#dcfce7; color:#166534; }
.stream-num.current { background:#fef3c7; color:#92400e; border:2px solid #f59e0b; }
</style>
</head>
<body>
<div class="container">
<h1>🔴 076. 数据流的中位数 <span class="badge hard">困难</span></h1>
<p class="subtitle">分类:堆 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label for="inputArea">输入:</label>
<input type="text" id="inputArea" value="stream=[1,2,3,4,5]" placeholder="stream=[1,2,3,...]">
<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 = [
{stream:[1,2,3,4,5], label:'示例1: [1,2,3,4,5]'},
{stream:[5,2,3,1,4], label:'示例2: [5,2,3,1,4] (乱序)'},
{stream:[1], label:'示例3: [1]'},
{stream:[-1,-2,-3,-4], label:'示例4: [-1,-2,-3,-4] (负数)'},
];
let stream, steps, stepCtrl;
function buildSteps(arr) {
stream = arr.slice();
steps = [];
const maxHeap = []; // left half (negated for max behavior)
const minHeap = []; // right half
function maxHeapPush(val) {
maxHeap.push(-val);
let i = maxHeap.length - 1;
while (i > 0) {
const p = (i-1) >> 1;
if (maxHeap[p] < maxHeap[i]) { [maxHeap[p], maxHeap[i]] = [maxHeap[i], maxHeap[p]]; i = p; }
else break;
}
}
function maxHeapPop() {
const top = maxHeap[0]; const last = maxHeap.pop();
if (maxHeap.length) { maxHeap[0] = last; let i = 0;
while (true) { let s=i; const l=2*i+1, r=2*i+2;
if (l < maxHeap.length && maxHeap[l] < maxHeap[s]) s = l;
if (r < maxHeap.length && maxHeap[r] < maxHeap[s]) s = r;
if (s !== i) { [maxHeap[i], maxHeap[s]] = [maxHeap[s], maxHeap[i]]; i = s; } else break; }
} return -top;
}
function minHeapPush(val) {
minHeap.push(val);
let i = minHeap.length - 1;
while (i > 0) {
const p = (i-1) >> 1;
if (minHeap[p] > minHeap[i]) { [minHeap[p], minHeap[i]] = [minHeap[i], minHeap[p]]; i = p; }
else break;
}
}
function minHeapPop() {
const top = minHeap[0]; const last = minHeap.pop();
if (minHeap.length) { minHeap[0] = last; let i = 0;
while (true) { let s=i; const l=2*i+1, r=2*i+2;
if (l < minHeap.length && minHeap[l] < minHeap[s]) s = l;
if (r < minHeap.length && minHeap[r] < minHeap[s]) s = r;
if (s !== i) { [minHeap[i], minHeap[s]] = [minHeap[s], minHeap[i]]; i = s; } else break; }
} return top;
}
function balance() {
// maxHeap size = minHeap size or maxHeap size = minHeap size + 1
if (maxHeap.length > minHeap.length + 1) {
minHeapPush(maxHeapPop());
} else if (minHeap.length > maxHeap.length) {
maxHeapPush(minHeapPop());
}
}
function getMedian() {
if (maxHeap.length === minHeap.length) {
return (-maxHeap[0] + minHeap[0]) / 2;
}
return -maxHeap[0];
}
function snapshot() {
return {
maxHeap: maxHeap.map(v => -v).sort((a,b) => b-a),
minHeap: minHeap.slice().sort((a,b) => a-b),
maxTop: maxHeap.length ? -maxHeap[0] : null,
minTop: minHeap.length ? minHeap[0] : null,
};
}
steps.push({stage:'init', msg:'初始化:大顶堆(左半)存较小一半,小顶堆(右半)存较大一半', added:0, current:null, ...snapshot()});
for (let i = 0; i < arr.length; i++) {
const num = arr[i];
// Decide which heap
if (maxHeap.length === 0 || num <= -maxHeap[0]) {
maxHeapPush(num);
steps.push({stage:'push-max', msg:`添加 ${num}:${num} ≤ 大顶堆顶(${maxHeap.length > 1 ? -maxHeap[1] : '∞'}),放入大顶堆(左半)`, added:i+1, current:num, ...snapshot()});
} else {
minHeapPush(num);
steps.push({stage:'push-min', msg:`添加 ${num}:${num} > 大顶堆顶(${-maxHeap[0] || '∞'}),放入小顶堆(右半)`, added:i+1, current:num, ...snapshot()});
}
// Balance
const beforeMax = maxHeap.length, beforeMin = minHeap.length;
balance();
if (maxHeap.length !== beforeMax || minHeap.length !== beforeMin) {
steps.push({stage:'balance', msg:`平衡:大顶堆 size=${beforeMax},小顶堆 size=${beforeMin} → 调整为大顶堆 size=${maxHeap.length},小顶堆 size=${minHeap.length}`, added:i+1, current:null, ...snapshot()});
}
const med = getMedian();
steps.push({stage:'median', msg:`当前中位数 = ${Number.isInteger(med) ? med : med.toFixed(1)},左半 ${maxHeap.length} 个,右半 ${minHeap.length} 个`, added:i+1, current:null, median:med, ...snapshot()});
// Record median for result
steps[steps.length - 1].medianVal = med;
}
steps.push({stage:'done', msg:'所有数据流处理完成!', added:arr.length, current:null, ...snapshot(), finalMedians: steps.filter(s => s.medianVal !== undefined).map(s => s.medianVal)});
}
function renderHeapCol(items, label, cls, topIdx) {
let html = `<div class="heap-col ${cls}"><h4>${label}</h4><div class="heap-items">`;
if (items.length === 0) {
html += '<span style="color:#94a3b8;font-style:italic;">空</span>';
}
items.forEach((v, i) => {
const topCls = cls === 'max-heap' ? (i === 0 ? 'top' : '') : (i === 0 ? 'top-min' : '');
const itemCls = cls === 'max-heap' ? 'max' : 'min';
html += `<span class="h-item ${itemCls} ${topCls}">${v}</span>`;
});
html += '</div></div>';
return html;
}
function render(step) {
const s = steps[step];
// Stream flow
let viz = '<div><b>数据流:</b></div><div class="stream-flow">';
for (let i = 0; i < stream.length; i++) {
let cls = 'pending';
if (i < s.added) cls = 'added';
if (s.current !== null && stream[i] === s.current && cls === 'added') cls = 'current';
viz += `<span class="stream-num ${cls}">${stream[i]}</span>`;
}
viz += '</div>';
// Dual heaps
viz += '<div class="dual-heap">';
viz += renderHeapCol(s.maxHeap, `大顶堆 (左/较小半) size=${s.maxHeap.length}`, 'max-heap', 0);
viz += renderHeapCol(s.minHeap, `小顶堆 (右/较大半) size=${s.minHeap.length}`, 'min-heap', 0);
viz += '</div>';
// Median display
if (s.median !== undefined) {
const medStr = Number.isInteger(s.median) ? s.median : s.median.toFixed(1);
viz += `<div class="median-box"><div class="label">当前中位数</div>${medStr}</div>`;
} else if (s.maxTop !== null) {
viz += `<div style="text-align:center;margin-top:8px;font-size:13px;color:#64748b;">大顶堆顶=${s.maxTop}${s.minTop !== null ? ',小顶堆顶=' + s.minTop : ''}</div>`;
}
$('vizArea').innerHTML = viz;
// Detail
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (s.maxTop !== null) {
detail += `<div style="margin-top:6px;font-size:13px;">大顶堆顶(左半最大): <code>${s.maxTop}</code>`;
if (s.minTop !== null) detail += ` | 小顶堆顶(右半最小): <code>${s.minTop}</code>`;
detail += '</div>';
detail += '<div style="font-size:13px;color:#64748b;">不变式:|左半大小 - 右半大小| ≤ 1,左半所有 ≤ 右半所有</div>';
}
$('detailContent').innerHTML = detail;
// Result
if (s.stage === 'done') {
const medians = s.finalMedians.map(m => Number.isInteger(m) ? m : m.toFixed(1));
$('resultContent').innerHTML = `<div class="final-answer">所有中位数序列: <b>[${medians.join(', ')}]</b><br><br>核心思路:大顶堆维护左半(较小),小顶堆维护右半(较大)<br>保证 |size差| ≤ 1 且 左半最大 ≤ 右半最小<br>中位数 = 堆顶元素的平均或大顶堆顶<br>时间 O(log n)/次,空间 O(n)</div>`;
}
$('hintText').textContent = s.msg;
const stages = ['init→初始化','push-max→入大顶堆','push-min→入小顶堆','balance→平衡','median→中位数','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].stream);
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(/stream=\[([^\]]+)\]/);
if (!m) { alert('格式: stream=[1,2,3,4,5]'); return; }
const arr = m[1].split(',').map(Number);
buildSteps(arr);
stepCtrl.setSteps(steps.map((_,i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = `stream=[${e.stream}]`;
buildSteps(e.stream);
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
class MedianFinder:
def __init__(self):
self.max_heap = [] # 左半(较小),存负数模拟大顶堆
self.min_heap = [] # 右半(较大),标准小顶堆
def addNum(self, num: int) -> None:
# 1. 决定放入哪个堆
if not self.max_heap or num <= -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
# 2. 平衡两个堆的大小
if len(self.max_heap) > len(self.min_heap) + 1:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def findMedian(self) -> float:
if len(self.max_heap) == len(self.min_heap):
return (-self.max_heap[0] + self.min_heap[0]) / 2
return -self.max_heap[0]
# 复杂度:addNum O(log n),findMedian O(1)`, {lang:'Python'});
})();
</script>
</body>
</html>