2 本のヒープで流れてくる数の中央値をすぐ答える仕組みを作る

数が​ 1 つずつ​届く​たびに​足して​おき、​いつ聞かれても、​それまでの​数の​中央値を​答える​仕組みを​作ります。​小さい​半分と​大きい​半分を​ 2 本の​ヒープに​分けて​持ち、​境目の​ 2 つだけを​見る​練習です。

海外で定番の問題リスト 4 本のうち 4 本に入っています (Blind 75、NeetCode 150、Grind 75、LeetCode Top Interview 150)

問題

計測の値が 1 つずつ届く場面で、届いた値の中央値をいつでも答えられる仕組みを、クラス StreamMedian として作ってください。次の 2 つの操作を持ちます。

  • add(num) は、整数 num を足します。何も返しません
  • median() は、それまでに足した数の中央値を返します

中央値は、足した数を小さい順に並べたときの真ん中の値です。数の個数が偶数のときは、真ん中の 2 つを足して 2 で割った値です。小数になることがあり、そのときは 2.5 のような数で返します。割り切れるときは 3 と返しても 3.0 と返してもかまいません。

メソッドの名前は、JavaScript と Python は上のまま、Go は Add、Median と頭を大文字にします。Go では newStreamMedian() で作り、Median は float64 を返します。

入力は、呼ぶ操作の名前の並び ops と、それぞれの引数の並び args で渡します。ops の最初はクラスの名前で、args の最初は空の並びです。出力は操作ごとの戻り値の並びで、作った所と add は null です。

  • median は、数が 1 つ以上足されたあとにだけ呼びます
  • num は -100000 以上 100000 以下です
  • 操作の回数は 50000 回以下で、add と median がどんな順で混ざってもかまいません

入力と出力の例

例 1

4 だけなら 4、4 と 10 なら真ん中の 2 つを足して 2 で割って 7、1 を足すと並びは 1、4、10 で 4 です。

入力

ops = ["StreamMedian", "add",
  "median", "add", "median",
  "add", "median"]
args = [[], [4], [], [10], [],
  [1], []]

出力

[null, null, 4, null, 7,
 null, 4]

例 2

3 と 6 の中央値は 4.5 です。同じ値も別々に数えます。

入力

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 つの書き方を書き切るだけでなく、「どの操作が何回呼ばれるか」「値がどんな範囲に来るか」で持ち方を選び直せることが見られています。

アルゴリズムのチートシートの「ハッシュマップ」に、​この​型の​要点を​表と​コードで​まとめています。

main.py
class StreamMedian:
    def __init__(self):
        # ここに持つ物を書きます
        pass

    def add(self, num):
        # ここに書きます
        pass

    def median(self):
        # ここに書きます
        pass


# 「実行」を押すと、下の行が動きます
obj = StreamMedian()
print(obj.add(4))

できているか「例で確かめる」を押すと、問題の例を全部この関数に渡して、通ったかをここに出します