This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user