問題
機械のTimeStore としてkey ごとに、
set(key, value, timestamp)は、時刻 timestampにkeyの値を valueにした ことを 覚えます。 前の 値は 消さずに残します get(key, timestamp)は、keyについて、 時刻が timestamp以下のsetのうち最も 新しい ものの 値を 返します。 そういう setが無ければ、 空の 文字列 ""を返します
入力は、ops と、args でops のargs のset は null です。
setに渡す時刻は、 どの keyかに関わらず、 呼ぶたびに 必ず前より 大きくなります getに渡す時刻は 自由で、 前より 小さくなる こともあります keyとvalueは英小文字と 数字の 1 文字以上 100 文字以下、 時刻は 1 以上 10000000 以下です - 操作の
回数は 200000 回以下です
入力と出力の例
例 1
時刻 3 に eco、fast にeco の頃です。
入力
ops = ["TimeStore", "set",
"get", "get", "set", "get",
"get"]
args = [[], ["mode", "eco", 3],
["mode", 2], ["mode", 5],
["mode", "fast", 8],
["mode", 8], ["mode", 6]]出力
[null, null, "", "eco", null,
"fast", "eco"]例 2
設定の
入力
ops = ["TimeStore", "set",
"set", "set", "get", "get",
"get", "get"]
args = [[], ["temp", "20", 1],
["fan", "low", 4],
["temp", "24", 7],
["temp", 6], ["fan", 3],
["fan", 100], ["temp", 7]]出力
[null, null, null, null, "20",
"", "low", "24"]例 3
一度も set していないset より
入力
ops = ["TimeStore", "get",
"set", "get", "get"]
args = [[], ["door", 5],
["door", "open", 10],
["door", 9], ["door", 10]]出力
[null, "", null, "", "open"]面接での聞かれ方
テックリードの
- 「時刻つきの
キーと 値の 置き場を 作ってください。 ある 時刻での 値を 引けるように」 - 「set の
時刻が 増えていく 決まりが 無かったら どうしますか」と、 前提を 外した ときの 持ち方を 聞かれます - 「履歴が
どんどん 増えますが、 記憶は どうしますか」 「古い 履歴を 消すなら どう 決めますか」と、 運用まで 続く こともあります - 「書き込みが
多い 場合と 読み出しが 多い 場合で、 持ち方を 変えますか」と、 取り 引きを 話させられます
時刻がset が
アルゴリズムのチートシートの「ハッシュマップ」に、この型の要点を表とコードでまとめています。
まず 考える手順
段 1
素直には、set をget のtimestamp 以下のget が O(n) に
段 2
まず名前ごとにset の
段 3
get は、timestamp よりtimestamp 以下でset です。timestamp 以下の set は
段 4
前提がset が O(n) です。set も get も O(log n) にします。
解答
Python
Python
from bisect import bisect_right
class TimeStore:
def __init__(self):
self.log = {}
def set(self, key, val, ts):
if key not in self.log:
pair = ([], [])
self.log[key] = pair
t, v = self.log[key]
t.append(ts)
v.append(val)
def get(self, key, ts):
if key not in self.log:
return ""
t, v = self.log[key]
i = bisect_right(t, ts)
if i == 0:
return ""
return v[i - 1]JavaScript
JavaScript
class TimeStore {
constructor() {
this.log = new Map();
}
set(key, val, ts) {
const d = this.log;
if (!d.has(key)) {
d.set(key, [[], []]);
}
const [t, v] = d.get(key);
t.push(ts);
v.push(val);
}
get(key, ts) {
const e = this.log.get(key);
if (!e) return "";
const [t, v] = e;
let lo = 0;
let hi = t.length;
while (lo < hi) {
const m = (lo + hi) >> 1;
if (t[m] <= ts) {
lo = m + 1;
} else {
hi = m;
}
}
if (lo === 0) return "";
return v[lo - 1];
}
}Go
Go
import "sort"
type history struct {
times []int
vals []string
}
type TimeStore struct {
log map[string]*history
}
func newTimeStore() *TimeStore {
return &TimeStore{
map[string]*history{},
}
}
func (s *TimeStore) Set(
key, val string, ts int,
) {
h := s.log[key]
if h == nil {
h = &history{}
s.log[key] = h
}
h.times = append(h.times, ts)
h.vals = append(h.vals, val)
}
func (s *TimeStore) Get(
key string, ts int,
) string {
h := s.log[key]
if h == nil {
return ""
}
i := sort.SearchInts(
h.times, ts+1,
)
if i == 0 {
return ""
}
return h.vals[i-1]
}Python の bisect_right(t, ts) と JavaScript のts よりsort.SearchInts はts+1 をts+1 以上と ts より
計算量
setの時間は 平均 O(1) です。 辞書を 引き、 並びの 末尾に 書き足すだけです getの時間は O(log m) です (m は その 名前の setの回数)。 辞書を 引くのは 平均 O(1) で、 あとは 二分探索です - 使う
記憶は O(n) です (n は setの回数の 合計)。 履歴を 消さずに 全部残すためです
よく ある間違い
- ちょうど
同じ 時刻を落とす。 「 timestamp以上の最初の 位置」を 探して その 1 つ前を 返すと、 例 1 の get("mode", 8)で、時刻 8 の fastではなくecoを返します。 探すのは 「 timestampより大きい 最初の 位置」です - 見つからない
ときに 番号 -1 を読む。 位置が 0 の ときに v[i - 1]を読むと、 Python では v[-1]、つまり 最も 新しい 値を 返してしまい、 エラーに なりません。 例 3 の get("door", 9)がopenになる、 気づきにくい間違いです - 名前ごとに
分けずに 1 本の 並びで 二分探索する。 全部の setを 1 本にまとめると、 時刻は 並んでいても、 見つけた 項目が 別の 名前の ことがあります。 例 2 の get("fan", 3)でtempの値を 返してしまいます - 値を
上書きして 1 つだけ持つ。 名前から 最新の 値だけを 持つ辞書に すると、 過去の 時刻を 聞かれた ときに 答えられません。 例 1 の get("mode", 6)は、すでに fastにした後の 呼び出しでも ecoを返す必要があります - 前提を
確かめずに 並べ替えを入れる。 setのたびに 並べ替えると、 時刻が 増える 順に 来る 前提を 使っておらず、 setが O(m log m) になります。 前提が あるなら 末尾に 足すだけ、 無いなら 差し込みか木、と 前提に 合わせて 選んだと 説明できるようにします
解答を動かした版 Python 3.12.14Node.js 22.23.4Go 1.24.6確認日 2026年10月30日
面接での 話し方の例
書き始める
「set の
時刻は 必ず 増えると 確かめたので、 名前ごとに 時刻の 並びと 値の 並びを 持ち、 set は 末尾に 足すだけにします。 時刻の 並びは 小さい 順のままなので、 get は 時刻より 大きい 最初の 位置を 二分探索で 探し、 その 1 つ前を 返します。 位置が 0 なら 空の 文字列です。 set は 平均 O(1)、 get は O(log m)、 記憶は set の 回数分の O(n) です。 時刻が 順に 来ない 前提なら、 名前ごとに 順序付きの 木に して、 set も get も O(log n) にします。」
書き終えたら、
「例 1 では、
mode の 時刻の 並びは 最後に [3, 8]です。get("mode", 6)では 6 より大きい 最初の 位置が 1 なので、 その前の 0 番の ecoを返します。 get("mode", 2)では最初の 位置が 0 なので、 まだ 何も set されていない 頃として 空の 文字列を 返します。」
