問題
計測のStreamMedian として
add(num)は、整数 numを足します。 何も 返しません median()は、それまでに 足した 数の 中央値を返します
中央値は、2.5 のような3 と3.0 と
メソッドのAdd、Median とnewStreamMedian() でMedian は float64 を
入力は、ops と、args でops のargs のadd は null です。
medianは、数が 1 つ以上足された あとにだけ呼びます numは -100000 以上 100000 以下です- 操作の
回数は 50000 回以下で、 addとmedianがどんな 順で 混ざってもかまいません
入力と出力の例
例 1
4 だけなら 4、
入力
ops = ["StreamMedian", "add",
"median", "add", "median",
"add", "median"]
args = [[], [4], [], [10], [],
[1], []]出力
[null, null, 4, null, 7,
null, 4]例 2
3 と 6 の
入力
ops = ["StreamMedian", "add",
"add", "median", "add",
"median", "add", "median"]
args = [[], [3], [6], [], [6],
[], [-2], []]出力
[null, null, null, 4.5, null,
6, null, 4.5]例 3
負の
入力
ops = ["StreamMedian", "add",
"add", "median", "add",
"median", "add", "median"]
args = [[], [-1], [-5], [],
[-9], [], [-20], []]出力
[null, null, null, -3, null,
-5, null, -7]面接での聞かれ方
面接では、
- 「
addとmedianが同じくらいの 回数で 呼ばれます。 どちらも 速くしてください」と、 両方の 重さを 条件にされます - 書き終えた
あとに 「値が すべて 0 以上 100 以下だと 分かっていたら、 どう 持ちますか?」 「ほとんどが その 範囲で、 とき どき外れる 値が 来るなら?」と、 値の 分布に 合わせた 持ち方を 聞かれます - 「直近の 1000 個だけの
中央値に するなら、 古い 値を どう 消しますか?」 「機械を 何台かに 分けるなら?」と、 仕組みを 広げた 先まで 話が進みます
テックリードとしては、
アルゴリズムのチートシートの「ハッシュマップ」に、この型の要点を表とコードでまとめています。
まず 考える手順
段 1
まずはmedian のadd は O(1) ですが、median は
段 2
並びをadd が O(n) です。median はadd の
段 3
中央値に
段 4
2 つの
段 5
median は、add はmedian は
段 6
ヒープを
解答
Python
Python
from heapq import heappop
from heapq import heappush
class StreamMedian:
def __init__(self):
self.lo = []
self.hi = []
def add(self, num):
lo = self.lo
hi = self.hi
heappush(lo, -num)
x = -heappop(lo)
heappush(hi, x)
if len(hi) > len(lo):
y = heappop(hi)
heappush(lo, -y)
def median(self):
lo = self.lo
hi = self.hi
a = -lo[0]
if len(lo) > len(hi):
return float(a)
b = hi[0]
return (a + b) / 2JavaScript
JavaScript
class StreamMedian {
constructor() {
this.lo = [];
this.hi = [];
}
add(num) {
const lo = this.lo;
const hi = this.hi;
heapPush(lo, -num);
const x = -heapPop(lo);
heapPush(hi, x);
if (hi.length > lo.length) {
const y = heapPop(hi);
heapPush(lo, -y);
}
}
median() {
const lo = this.lo;
const hi = this.hi;
const a = -lo[0];
if (lo.length > hi.length) {
return a;
}
return (a + hi[0]) / 2;
}
}
function heapPush(h, x) {
h.push(x);
let i = h.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (h[p] <= h[i]) {
break;
}
[h[p], h[i]] = [h[i], h[p]];
i = p;
}
}
function heapPop(h) {
const top = h[0];
const last = h.pop();
const n = h.length;
if (n === 0) {
return top;
}
h[0] = last;
let i = 0;
while (true) {
let m = i;
const l = 2 * i + 1;
const r = l + 1;
if (l < n && h[l] < h[m]) {
m = l;
}
if (r < n && h[r] < h[m]) {
m = r;
}
if (m === i) {
return top;
}
[h[m], h[i]] = [h[i], h[m]];
i = m;
}
}Go
Go
type StreamMedian struct {
lo []int
hi []int
}
func newStreamMedian(
) *StreamMedian {
return &StreamMedian{}
}
func (s *StreamMedian) Add(
num int,
) {
heapPush(&s.lo, -num)
x := -heapPop(&s.lo)
heapPush(&s.hi, x)
if len(s.hi) > len(s.lo) {
y := heapPop(&s.hi)
heapPush(&s.lo, -y)
}
}
func (s *StreamMedian) Median(
) float64 {
a := -s.lo[0]
if len(s.lo) > len(s.hi) {
return float64(a)
}
b := s.hi[0]
return float64(a+b) / 2
}
func heapPush(h *[]int, x int) {
a := append(*h, x)
i := len(a) - 1
for i > 0 {
p := (i - 1) / 2
if a[p] <= a[i] {
break
}
a[p], a[i] = a[i], a[p]
i = p
}
*h = a
}
func heapPop(h *[]int) int {
a := *h
top := a[0]
n := len(a) - 1
a[0] = a[n]
a = a[:n]
i := 0
for {
m := i
l := 2*i + 1
r := l + 1
if l < n && a[l] < a[m] {
m = l
}
if r < n && a[r] < a[m] {
m = r
}
if m == i {
break
}
a[m], a[i] = a[i], a[m]
i = m
}
*h = a
return top
}lo にはlo のheapq をheapPush と heapPop で
計算量
- 時間は、
addが O(log n)、medianが O(1) です。addはヒープへの 出し入れを 最大 3 回し、 それぞれ木の 高さの O(log n) です - 使う
記憶は、 足した 数の 個数を n として O(n) です。 どの 数も 2 本の ヒープの どちらか 一方に 1 回だけ入ります - 値の
範囲が 0 以上 100 以下のように 小さく 決まっているなら、 値ごとの 個数を 数える 101 個の 表で 持つ手も あります。 addは O(1)、medianは表を 前から 数えて O(101) で、 値の 範囲が 狭いほど得です。 範囲の 外の 値が まれに 来るなら、 外れた 値だけ別の ヒープや 並びに 分けて 持ち、 表と 組み合わせます
よく ある間違い
- 新しい
数を、 個数の 少ない ほうの ヒープへ そのまま入れる。 個数は そろいますが、 2 つの 半分の 境目が 崩れます。 例 3 で -1 の あとの -5 を 大きい半分へ 入れると、 小さい 半分の -1 より 小さい 数が 大きい 半分に 入り、 -9 を 足した あとの 中央値が -5 ではなく -1 に なります。 いったん反対の ヒープを 通してから入れます - 偶数の
ときの 割り算を 整数の 割り算にする。 Python の //や、Go の 整数どうしの 割り算では、 4.5 が 4 に なります。 負の 数では さらに 外れ、 -1 と -2 の 中央値 -1.5 が、 Python の //では -2、Go の 整数の 割り算では -1 に なります。 小数で割ります - 最大ヒープの
符号の 戻し忘れ。 小さい 半分には 符号を 逆に して 入れているので、 先頭を そのまま 使うと、 中央値の 符号が 逆に なります。 取り出すときと 見る ときは、 必ず 符号を戻します
解答を動かした版 Python 3.12.14Node.js 22.23.4Go 1.24.6確認日 2026年10月30日
面接での 話し方の例
書き始める
「毎回並べ替えると
medianが O(n log n)、並びを 保つと addが O(n) です。中央値に 要るのは 真ん中の 境目だけなので、 小さい 半分を 最大ヒープ、 大きい 半分を 最小ヒープに 分けます。 小さい 半分は 大きい半分以下で、 個数は 同じか 小さい 半分が 1 つ 多い、を 守ります。 addは O(log n)、medianは O(1) です。値の 範囲が 狭いと 分かっていれば、 数える 表で 持つ手も あります。」
書き終えたら、
「例 2 で
たどります。 3 を 足すと 小さい 半分が [3]、6 を 足すと 一度小さい 半分を 通って 大きい半分へ 行き [3]と[6]で、中央値は 足して 2 で 割った 4.5 です。 もう 1 つの 6 は 小さい 半分を 通って 大きい半分へ 移り、 大きい 半分が 多くなるので 先頭の 6 を 戻して [3, 6]と[6]、中央値は 6 です。 -2 を 足すと [-2, 3]と[6, 6]で、境目の 3 と 6 から 4.5 です。」
