問題
容量 capacity のLRUCache として
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 が
入力
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 だけなら 読み取りの ロックで 足りますか」と、 並行の 扱いを 聞かれます - 「標準の
順序付き辞書を 使って よいなら、 どう 書きますか。 使わないなら、 中で 何が 起きていますか」と、 道具に 頼った 書き方の 中身を 説明させられる こともあります
書き始める
アルゴリズムのチートシートの「ハッシュマップ」に、この型の要点を表とコードでまとめています。
まず 考える手順
段 1
要る
段 2
使った
段 3
辞書にはhead と tail) を
段 4
put のtail の 1 つ前の
段 5
道具にOrderedDict はmove_to_end(key) でpopitem(last=False) で
解答
Python
Python
from types import (
SimpleNamespace,
)
def new_node(k, v):
return SimpleNamespace(
key=k, val=v,
prev=None, next=None,
)
def unlink(nd):
nd.prev.next = nd.next
nd.next.prev = nd.prev
def to_front(head, nd):
nd.prev = head
nd.next = head.next
head.next.prev = nd
head.next = nd
class LRUCache:
def __init__(self, cap):
self.cap = cap
self.map = {}
h = new_node(0, 0)
t = new_node(0, 0)
h.next = t
t.prev = h
self.head = h
self.tail = t
def get(self, key):
nd = self.map.get(key)
if nd is None:
return -1
unlink(nd)
to_front(self.head, nd)
return nd.val
def put(self, key, val):
if self.cap == 0:
return
m = self.map
if key in m:
unlink(m.pop(key))
if len(m) == self.cap:
old = self.tail.prev
unlink(old)
del m[old.key]
nd = new_node(key, val)
m[key] = nd
to_front(self.head, nd)JavaScript
JavaScript
function newNode(key, val) {
return {
key, val,
prev: null, next: null,
};
}
function unlink(nd) {
nd.prev.next = nd.next;
nd.next.prev = nd.prev;
}
function toFront(head, nd) {
nd.prev = head;
nd.next = head.next;
head.next.prev = nd;
head.next = nd;
}
class LRUCache {
constructor(capacity) {
this.cap = capacity;
this.map = new Map();
this.head = newNode(0, 0);
this.tail = newNode(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(k) {
const nd = this.map.get(k);
if (!nd) return -1;
unlink(nd);
toFront(this.head, nd);
return nd.val;
}
put(k, v) {
if (this.cap === 0) return;
const { head, tail } = this;
const m = this.map;
if (m.has(k)) {
unlink(m.get(k));
m.delete(k);
}
if (m.size === this.cap) {
const old = tail.prev;
unlink(old);
m.delete(old.key);
}
const nd = newNode(k, v);
m.set(k, nd);
toFront(head, nd);
}
}Go
Go
type lruNode struct {
key, val int
prev, next *lruNode
}
func unlink(n *lruNode) {
n.prev.next = n.next
n.next.prev = n.prev
}
func toFront(
h, n *lruNode,
) {
n.prev, n.next = h, h.next
h.next.prev = n
h.next = n
}
type LRUCache struct {
cap int
m map[int]*lruNode
head, tail *lruNode
}
func newLRUCache(
capacity int,
) *LRUCache {
h := &lruNode{}
t := &lruNode{}
h.next, t.prev = t, h
return &LRUCache{
cap: capacity,
m: map[int]*lruNode{},
head: h,
tail: t,
}
}
func (c *LRUCache) Get(
key int,
) int {
n, ok := c.m[key]
if !ok {
return -1
}
unlink(n)
toFront(c.head, n)
return n.val
}
func (c *LRUCache) Put(
key, value int,
) {
if c.cap == 0 {
return
}
if n, ok := c.m[key]; ok {
unlink(n)
delete(c.m, key)
}
if len(c.m) == c.cap {
old := c.tail.prev
unlink(old)
delete(c.m, old.key)
}
n := &lruNode{key: key}
n.val = value
c.m[key] = n
toFront(c.head, n)
}節をunlink とtoFront (Python は to_front) は、SimpleNamespace で
Go はcontainer/list を*list.Element にし、MoveToFront と Back、Remove を
計算量
getとputの時間は、 どちらも 平均 O(1) です。 辞書を 引くのが 平均 O(1)、 節を 外して 先頭へ 付けるのは 決まった 本数の つなぎ替えだけです - 使う
記憶は O(capacity) です。 辞書の 項目と 節が、 どちらも 容量の 数までしか 増えません - 「平均」と
付けるのは 辞書の ためです。 鍵の 散らばりが 悪いと 辞書の 引きが 遅くなる 場合が あり、 面接では そこまで 言えると丁寧です
よく ある間違い
- get で
順を 動かさない。 getは値を 読むだけに 見えますが、 「使った」ことに なるので 先頭へ 動かします。 動かし忘れると、 例 1 で 3 を 入れる ときに 1 を 捨ててしまい、 最後の get(1)が -1 になります - 同じ鍵の put で
新しい 節を足す。 上書きの ときに 古い節を 残したまま 新しい 節を 足すと、 辞書は 1 つなのに リストには 2 つ 残り、 数え方が ずれて、 まだ 余裕が あるのに 捨てたり、 捨てたはずの 鍵の 節が 残ったりします。 例 2 の put(1, 5)は、古い節を 外してから 新しい 節を 先頭に 付けるので、 数は 2 のままです - 捨てる
ときに 辞書から 消し忘れる。 リストから 外しても 辞書に 残ると、 getが捨てたはずの 値を 返します。 節に 鍵を 持たせるのは、 このためです - 容量 0 を
考えない。 「いっぱいなら 捨ててから 入れる」順で 書くと、 容量 0 では 空の うちから 「いっぱい」と 判断されます。 捨てる 節は tailの 1 つ前ですが、空の リストでは それが headの番兵なので、 番兵を 外そうとして 壊れます。 最初に 「容量 0 なら 何もしない」と 決めておきます - 並行の
扱いで get を 読み取りだけと考える。 複数の スレッドから 使う とき、 getもリストの つなぎ替えを するので 書き込みです。 読み取り用の ロックだけで 守ると、 つなぎ替えの 途中を 別の スレッドが 見て、 リストが 壊れます。 1 つの ロックで getとputを丸ごと 守るのが 基本で、 混むなら鍵で 分けた 小さい キャッシュを 複数持つ (分割する ) 方法を話します - 道具の
名前だけで 済ませる。 OrderedDictやLinkedHashMapで書いた あとに 「中は どうなっていますか」と 聞かれて 答えられないと、 テックリードの 面接では 評価されません。 辞書と 双方 向リストの 組である ことを 説明できるように しておきます
解答を動かした版 Python 3.12.14Node.js 22.23.4Go 1.24.6確認日 2026年10月30日
面接での 話し方の例
書き始める
「要るのは、
鍵で 探す、 使った 鍵を 最新に する、 最も 古い鍵を 捨てる、の 3 つで、 全部 O(1) に したいです。 辞書だけだと 最も 古い 鍵が 分からず、 配列で 順を 持つと 途中を 動かすたびに O(n) かかります。 そこで、 使った 順を 双方向の 連結リストで 持ち、 辞書には 鍵から その 節への 参照を 持ちます。 両端に 番兵を 置けば、 外すのも 先頭に 付けるのも つなぎ替えだけで O(1) です。 容量 0 は 最初に 何もしないと 決め、 同じ鍵の put は 古い節を 外して 付け直すので、 数は 増えず、 最近使った方に なります。 記憶は O(capacity) です。」
書き終えたら、
「例 1 では、
1 と 2 を 入れた あと get(1)で 1 が先頭に 来るので、 末尾は 2 です。 3 を 入れると 容量を 超えるので、 末尾の 2 を 外して 辞書からも 消し、 3 を 先頭に 付けます。 だから get(2)は -1、get(1)は 10 です。複数の スレッドから 使うなら、 get も 順を 書き換えるので、 get と put を 同じロックで 守ります。 混むようなら、 鍵の 散らし方で 分けた 小さい キャッシュを 並べて ロックを 分けます。」
