Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Probabilistic & System-Scale Structures  ›  Lesson

LRU, LFU and What Actually Hits

Cache Eviction 26 minA doubly-linked list plus a hash map gives O(1) LRU; LFU evicts by frequency instead, and neither policy is automatically better
You're building a piece ofQuarry — the storage engine
This piece — lru_vs_fifo(): Measures LRU against FIFO on the same access trace before picking the read cache's policy.
Scenario Quarry's engineers are debating whether to switch the read cache from FIFO to LRU, and want to see both policies measured on the exact same access trace before deciding.
Your task
Build lru_vs_fifo(capacity, keys). Simulate both an LRU cache and a FIFO cache over the same keys trace, and return {"lru_hits": ..., "fifo_hits": ...}. Example: lru_vs_fifo(2, [2, 3, 2, 1, 3]) -> {"lru_hits": 1, "fifo_hits": 2}.

LRU, LFU and What Actually Hits

In short: an LRU (least-recently-used) cache keeps a doubly-linked list in recency order, plus a hash map from key to its node, so every operation is O(1): a hit splices its node to the front (MRU end), a miss evicts the tail (LRU end) and inserts the new key at the front. LFU (least-frequently-used) evicts by hit count instead of recency, breaking ties by recency. Neither is free: LRU can be worse than plain FIFO on some access patterns, because promoting a hit to the front pushes some other key closer to eviction — a key that FIFO, which never reorders on a hit, would have kept.

A doubly-linked chain: hot at one end, doomed at the other — a hit splices its node to the front in 3 pointer rewires, a miss evicts the tail

step through it
1class Node:
2 def __init__(self, key=None, value=None):
3 self.key, self.value = key, value
4 self.prev = self.next = None
5 
6def order_after_each_touch(capacity, keys):
7 head, tail = Node(), Node()
8 head.next, tail.prev = tail, head
9 nodes = {}
10 
11 def remove(n):
12 n.prev.next, n.next.prev = n.next, n.prev
13 
14 def insert_front(n):
15 n.next, n.prev = head.next, head
16 head.next.prev, head.next = n, n
17 
18 for k in keys:
19 if k in nodes:
20 remove(nodes[k])
21 insert_front(nodes[k])
22 else:
23 if len(nodes) >= capacity:
24 victim = tail.prev
25 remove(victim)
26 del nodes[victim.key]
27 n = Node(k, k)
28 nodes[k] = n
29 insert_front(n)
30 cur, seen = head.next, []
31 while cur is not tail:
32 seen.append(cur.key)
33 cur = cur.next
34 print(f"touch {k}: {seen}")
35 
36order_after_each_touch(2, [2, 3, 2, 1, 3])

LRU can lose to a policy that doesn't even look

Quarry compares two eviction policies on the exact same trace: LRU, and plain FIFO (evict whatever arrived first — no reordering on a hit at all):

python

FIFO gets 2 hits; LRU gets only 1. Trace it: after 2, 3, both policies hold {2, 3}. The third touch, 2, is a hit for both — but LRU promotes 2 to the front, which makes 3 the new least-recently-used. When 1 arrives next (a miss for both), LRU evicts 3 — the exact key 1's successor, 3 again, needs a step later. FIFO never reordered on the hit, so it evicts 2 (the oldest insertion) instead, and 3 survives to be a hit. Reacting to a hit is not automatically the safer choice.

Break an LFU tie by forgetting to update it

LFU breaks a frequency tie by recency — whichever tied key was touched longest ago goes. That tiebreak only works if a hit keeps the key's last_used clock current, not just its frequency count:

broken — fix it

This evicts "a" by mistake when "c" arrives, so the final lookup for "a" misses instead of hits. Add the missing last_used update to the hit branch.

With both branches updating last_used, "a" keeps winning the tie against "b" (touched more recently, at step 4 vs step 3), "b" is the one evicted when "c" needs room, and the final a lookup is a hit. Forgetting the update doesn't crash anything — it just silently evicts the wrong key every time a tie comes up, which is worse than crashing: nothing flags it.

Quick practice

What does this print?
order = ["c", "a", "b"]
order.remove("b")
order.insert(0, "b")
print(order)
broken — fix it

This evicts the wrong end of the queue, so "2" gets kicked out early and misses on its second visit instead of hitting. Fix the one call.


🎯 Your turn

Build lru_vs_fifo(capacity, keys): simulate both an LRU cache and a FIFO cache over the same keys trace, and return {"lru_hits": ..., "fifo_hits": ...}.

  • lru_vs_fifo(2, [2, 3, 2, 1, 3]) → {"lru_hits": 1, "fifo_hits": 2}

Hint: build simulate_lru_list and simulate_fifo first (a plain list is fine for LRU here — the real O(1) doubly-linked version is the boss exercise below), then sum each one's hit list.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upPromote on a hit

Write move_to_front(order, key), returning a new list with key moved to the front (inserted at the front if it wasn't present at all). move_to_front(['a','b','c'], 'c') -> ['c', 'a', 'b'].

arrays
DrillFIFO: never reorders

Write simulate_fifo(capacity, keys), returning a hit/miss list. A hit changes nothing. A miss, if full, evicts the OLDEST inserted key, then appends the new one. simulate_fifo(2, [1,2,1,3,2]) -> [False, False, True, False, True].

cache-eviction
BuildLRU with a plain list

Write simulate_lru_list(capacity, keys), returning a hit/miss list. A hit moves the key to the front. A miss, if full, evicts the LAST (least-recently-used) item, then inserts the new key at the front. simulate_lru_list(2, [2,3,2,1,3]) -> [False, False, True, False, False].

cache-eviction
BossThe real O(1) LRU cache

Write run_lru_trace(capacity, operations) using a real doubly-linked list plus a dict of key -> node, giving O(1) get and put. Each operation is ('get', key) or ('put', key, value). Return the list of results for every get (the value, or None on a miss). run_lru_trace(2, [('put',1,1),('put',2,2),('get',1),('put',3,3),('get',2)]) -> [1, None].

linked-listshash-tables
CapstoneLFU: evict by frequency

Write simulate_lfu(capacity, keys), returning a hit/miss list. A hit increments that key's use count. A miss, if full, evicts the key with the SMALLEST use count (ties broken by least-recently-touched), then inserts the new key. simulate_lfu(2, [1,2,1,3,1,2]) -> [False, False, True, False, True, False].

cache-eviction
lru_vs_fifo.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
lru_vs_fifo(capacity, keys) → dictHow many hits LRU and FIFO each get over the same access trace.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.