問題
抽選にRandomSet として
insert(val)は、valがまだ 入っていなければ 足して 真を 返します。 もう 入っていれば 何も せず偽を返します remove(val)は、valが入っていれば 取り除いて 真を 返します。 入っていなければ 何も せず偽を返します getRandom()は、いま入っている 値から 1 つを 選んで 返します。 どの 値も 同じ 確率で 選ばれるようにします
メソッドのget_random のようにInsert、Remove、GetRandom とnewRandomSet() で
入力は、ops と、args でops のargs のnull です。
getRandomは、値が 1 つ以上入っている ときだけ 呼びます。 答えを 1 つに 決めて 比べる ため、 例と 確かめでは、 値が ちょうど 1 つ 入っている ときだけ呼びます valは -2147483648 以上 2147483647 以下です- 操作の
回数は 200000 回以下です
入力と出力の例
例 1
4 は 2 回目の insert では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]面接での聞かれ方
面接では、
- 最初に
「集合 (set) を 使います」と 言うと、 「では、 集合から 同じ 確率で 1 つを O(1) で 選べますか?」と 聞かれます - 並びだけで
持つと、 「途中の 値を 取り除くのは O(n) では ありませんか?」と 指摘されます - 書き終えた
あとに 「同じ 値を いく つも 入れてよい 形に 変えるなら、 何を 持ちますか?」と続きます
辞書は
アルゴリズムのチートシートの「ハッシュマップ」に、この型の要点を表とコードでまとめています。
まず 考える手順
段 1
まずはinsert と remove はgetRandom が O(n) に
段 2
並びだけでgetRandom はremove は、
段 3
2 つをvals にpos でinsert は、getRandom は、
段 4
remove のi をi のi に
解答
Python
Python
from random import choice
class RandomSet:
def __init__(self):
self.pos = {}
self.vals = []
def insert(self, val):
pos = self.pos
if val in pos:
return False
vals = self.vals
pos[val] = len(vals)
vals.append(val)
return True
def remove(self, val):
pos = self.pos
if val not in pos:
return False
vals = self.vals
i = pos.pop(val)
last = vals.pop()
if i < len(vals):
vals[i] = last
pos[last] = i
return True
def get_random(self):
return choice(self.vals)JavaScript
JavaScript
class RandomSet {
constructor() {
this.pos = new Map();
this.vals = [];
}
insert(val) {
const pos = this.pos;
if (pos.has(val)) {
return false;
}
const vals = this.vals;
pos.set(val, vals.length);
vals.push(val);
return true;
}
remove(val) {
const pos = this.pos;
if (!pos.has(val)) {
return false;
}
const vals = this.vals;
const i = pos.get(val);
pos.delete(val);
const last = vals.pop();
if (i < vals.length) {
vals[i] = last;
pos.set(last, i);
}
return true;
}
getRandom() {
const vals = this.vals;
const n = vals.length;
const i = Math.floor(
Math.random() * n
);
return vals[i];
}
}Go
Go
import "math/rand"
type RandomSet struct {
pos map[int]int
vals []int
}
func newRandomSet() *RandomSet {
return &RandomSet{
pos: map[int]int{},
}
}
func (s *RandomSet) Insert(
val int,
) bool {
if _, ok := s.pos[val]; ok {
return false
}
s.pos[val] = len(s.vals)
s.vals = append(s.vals, val)
return true
}
func (s *RandomSet) Remove(
val int,
) bool {
i, ok := s.pos[val]
if !ok {
return false
}
delete(s.pos, val)
n := len(s.vals) - 1
last := s.vals[n]
s.vals = s.vals[:n]
if i < n {
s.vals[i] = last
s.pos[last] = i
}
return true
}
func (s *RandomSet) GetRandom(
) int {
i := rand.Intn(len(s.vals))
return s.vals[i]
}remove は、i がi へ
計算量
- 時間は、
3 つの 操作の どれも 平均 O(1) です。 辞書の 出し入れは 平均 O(1)、 並びの 末尾に 足す、 末尾を 取り除く、 番号で 引く、は どれも O(1) です - 使う
記憶は、 入っている 値の 数を n として O(n) です。 並びと 辞書に 1 つずつ持ちます - 並びの
途中を 抜いて 詰める 書き方だと removeは O(n) です。末尾と 入れ替える ことで、 並びの 順を 捨てる 代わりに O(1) に しています。 この 集合は 順を 約束しないので、 捨ててかまいません
よく ある間違い
- 末尾を
移した あと、 移した 値の 番号を 辞書に 書き直さない。 辞書は 古い 末尾の 番号を 指したままに なり、 その値を 次に 取り除く とき、 並びの 外か別の 値の 位置を 消します。 例 3 で 3 を 取り除く ところが壊れます - 取り除く
値が 末尾だった ときを 考えない。 辞書から 先に 消し、 その あと 分けずに 「末尾を iへ移して 番号を 書く」と、 末尾の 値は 取り除く 値その ものなので、 消したはずの 値が 辞書に 戻ります。 次の insertで偽が 返り、 足せなくなります。 末尾より 前の ときだけ移すか、 番号の 書き直しを 先に して 辞書から 消すのを 後にします - 乱数の
番号の 範囲を 1 つずらす。 並びの 長さを n として、 番号は 0 以上 n 未満です。 Math.floor(Math.random() * (n - 1))のように書くと、 末尾の 値が 選ばれなくなり、 確率が 同じに なりません
解答を動かした版 Python 3.12.14Node.js 22.23.4Go 1.24.6確認日 2026年10月30日
面接での 話し方の例
書き始める
「集合だけでは
無作為に 選べず、 並びだけでは 途中を 消すのが O(n) です。 値は 並びに 入れ、 辞書で 値から 番号を 引けるようにします。 消すときは 末尾の 値を 消す位置へ 移して 末尾を 落とし、 移した 値の 番号を 書き直します。 3 つの 操作は どれも 平均 O(1)、 記憶は O(n) です。」
書き終えたら、
「例 3 で
たどります。 1、 2、 3 を 足すと、 並びは [1, 2, 3]です。1 を 消すと、 末尾の 3 を 0 番へ 移して [3, 2]、辞書の 3 は 0 番に 直します。 次に 3 を 消すと、 0 番に 末尾の 2 を 移して [2]です。残りは 2 だけなので、 getRandomは 2 を返します。」
