辞書と並びを組み合わせて O(1) で出し入れと無作為な取り出しができる集合を作る

設計問題 79

値を​足す、​取り除く、​入っている​値から​ 1 つを​同じ​確率で​選ぶ、の​ 3 つを、​どれも​平均 O(1) で​こなす集合を​作ります。​辞書と​並びの​ 2 つを​持ち、​取り除く​ときは​末尾と​入れ替えて​穴を​作らない​練習です。

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

問題

抽選に使う整数の集合を、クラス RandomSet として作ってください。同じ値は 1 つしか入りません。次の 3 つの操作を、どれも平均 O(1) の時間でこなしてください。

  • insert(val) は、val がまだ入っていなければ足して真を返します。もう入っていれば何もせず偽を返します
  • remove(val) は、val が入っていれば取り除いて真を返します。入っていなければ何もせず偽を返します
  • getRandom() は、いま入っている値から 1 つを選んで返します。どの値も同じ確率で選ばれるようにします

メソッドの名前は、JavaScript は上のまま、Python は get_random のように小文字と下線、Go は Insert、Remove、GetRandom と頭を大文字にします。Go では newRandomSet() で作ります。

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

  • getRandom は、値が 1 つ以上入っているときだけ呼びます。答えを 1 つに決めて比べるため、例と確かめでは、値がちょうど 1 つ入っているときだけ呼びます
  • val は -2147483648 以上 2147483647 以下です
  • 操作の回数は 200000 回以下です

入力と出力の例

例 1

4 は 2 回目の insert では足されません。4 を取り除くと 9 だけが残るので、getRandom は 9 です。

入力

ops = ["RandomSet", "insert",
  "insert", "insert",
  "remove", "getRandom",
  "remove"]
args = [[], [4], [4], [9],
  [4], [], [4]]

出力

[null, true, false, true,
 true, 9, false]

例 2

空の集合から取り除こうとすると偽です。取り除いた値は、もう一度足せます。

入力

ops = ["RandomSet", "remove",
  "insert", "getRandom",
  "remove", "insert"]
args = [[], [1], [-3], [],
  [-3], [-3]]

出力

[null, false, true, -3,
 true, true]

例 3

途中の値を取り除くと、末尾の値がその位置へ移ります。移った値もきちんと取り除けます。

入力

ops = ["RandomSet", "insert",
  "insert", "insert",
  "remove", "remove",
  "getRandom", "insert"]
args = [[], [1], [2], [3],
  [1], [3], [], [3]]

出力

[null, true, true, true,
 true, true, 2, true]

面接での聞かれ方

面接では、抽選や、たくさんの機械から 1 台を無作為に選んで仕事を振る仕組みを例に出して渡されることが多いです。

  • 最初に「集合 (set) を使います」と言うと、「では、集合から同じ確率で 1 つを O(1) で選べますか?」と聞かれます
  • 並びだけで持つと、「途中の値を取り除くのは O(n) ではありませんか?」と指摘されます
  • 書き終えたあとに「同じ値をいくつも入れてよい形に変えるなら、何を持ちますか?」と続きます

辞書は「あるかどうか」に強く、並びは「何番目かで選ぶ」のに強い、と道具の得意を分けて組み合わせられるかが見られています。

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

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

    def insert(self, val):
        # ここに書きます
        pass

    def remove(self, val):
        # ここに書きます
        pass

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


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

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