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

335 lines
14 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>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>