辞書と双方向リストで LRU キャッシュを設計する

容量が​決まった​覚え場所で、​いっぱいに​なったら​最も​長く​使われていない​物から​捨てる​仕組みを​作る。​辞書で​鍵から​節へ​一度で​飛び、​双方​向リストで​使った​順を​並べると、​読むのも​書くのも​平均 O(1) で​済みます。

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

問題

容量 capacity の覚え場所 (キャッシュ) を、クラス LRUCache として作ってください。鍵と値はどちらも整数です。次の 2 つの操作を持ちます。

  • get(key) は、key が入っていればその値を返し、無ければ -1 を返します。値を返したときは、その鍵を「いちばん最近使った」ことにします
  • put(key, value) は、key に value を入れます。すでに key があれば値を上書きし、その鍵を「いちばん最近使った」ことにします。新しい鍵を入れると容量を超えるときは、先に「最も長く使われていない」鍵を 1 つ捨ててから入れます
  • get と put は、どちらも平均 O(1) の時間で動くようにしてください

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

  • capacity は 0 以上 3000 以下です。0 のときは何も覚えず、put は何もしません
  • 鍵は 0 以上 10000 以下、値は 0 以上 100000 以下です
  • 操作の回数は 200000 回以下です

入力と出力の例

例 1

容量 2 です。get(1) で 1 が最近使った方になるので、3 を入れるときに捨てられるのは 2 です。

入力

ops = ["LRUCache", "put", "put",
  "get", "put", "get", "get",
  "get"]
args = [[2], [1, 10], [2, 20],
  [1], [3, 30], [2], [1], [3]]

出力

[null, null, null, 10, null,
 -1, 10, 30]

例 2

同じ鍵に入れ直すと、値が替わり、最近使った方にもなります。数は増えないので、ここではまだ何も捨てません。

入力

ops = ["LRUCache", "put", "put",
  "put", "put", "get", "get",
  "get"]
args = [[2], [1, 1], [2, 2],
  [1, 5], [3, 3], [1], [2], [3]]

出力

[null, null, null, null, null,
 5, -1, 3]

例 3

容量 0 では、入れた物はすぐに見つからなくなります。

入力

ops = ["LRUCache", "put", "get"]
args = [[0], [7, 70], [7]]

出力

[null, null, -1]

面接での聞かれ方

テックリードの面接では、コードの前に「どう持つか」を話させ、書いたあとに仕組みの外側まで掘られます。

  • 「容量付きのキャッシュを作ってください。いっぱいになったら、最も長く使われていない物を捨てます。get も put も O(1) で」
  • 「なぜ辞書だけ、リストだけではだめなのですか」「片方向のリストではだめですか」と、データの持ち方の理由を聞かれます
  • 「同じ鍵に put したら、捨てる順はどうなりますか」「容量 0 なら?」と、端の場合を詰められます
  • 「複数のスレッドから同時に使うなら?」「get だけなら読み取りのロックで足りますか」と、並行の扱いを聞かれます
  • 「標準の順序付き辞書を使ってよいなら、どう書きますか。使わないなら、中で何が起きていますか」と、道具に頼った書き方の中身を説明させられることもあります

書き始める前に、容量 0 の扱い、同じ鍵の上書きで「使った」ことにするか、鍵と値の型を確かめます。ここで決めたことを、そのまま問題の条件として話してから書くと、設計の話として評価されます。

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

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

    def get(self, key):
        # ここに書きます
        pass

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


# 「実行」を押すと、下の行が動きます
obj = LRUCache(2)
print(obj.put(1, 10))

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