Beyond Code & Karma · Data Structures
Keep only what was recently touched; release the rest. The O(1) data structure behind LeetCode 146 — explained through hash maps, doubly-linked lists, and the law of impermanence.
Chapter 01 — The Foundation
A cache stores a limited number of items so repeat lookups are fast.
When it fills up it must drop something to make room — and
the eviction policy decides what. An
LRU cache evicts the item accessed least
recently, betting that data you touched recently is data you'll
touch again soon. Both get and put count
as "using" a key, so each operation also refreshes that key's
recency.
The Cosmic Connection
In Vedic thought, anitya — impermanence — is the nature of all things: what arises must pass. The mind keeps smriti (living memory) of what attention lately touched, and lets the long-untouched slip into vismriti (forgetting). A cache is exactly this discipline, made mechanical.
"Hold the present lightly, hold nothing forever — and the structure stays clear, and O(1)."
Chapter 02 — Show Me The Code
There's a slick JavaScript shortcut and there's the structure interviewers actually want to see. Know both.
JavaScript's Map already iterates in insertion order.
Delete-then-set moves a key to the "newest" end, and
map.keys().next().value is always the oldest. That's a
full LRU cache in ~15 lines:
// The elegant JS trick: a Map already remembers insertion order. class LRUCache { constructor(capacity) { this.cap = capacity; this.map = new Map(); // key → value, O(1) lookup } get(key) { if (!this.map.has(key)) return -1; const v = this.map.get(key); this.map.delete(key); // unlink… this.map.set(key, v); // …re-insert → now most-recently-used return v; } put(key, value) { if (this.map.has(key)) this.map.delete(key); else if (this.map.size >= this.cap) this.map.delete(this.map.keys().next().value); // evict LRU (oldest) this.map.set(key, value); } }
keys() is therefore the least-recently-used. Elegant —
but it hides the machinery an interviewer wants you to explain.
Under the hood, that Map trick is a hash map
over a linked list. Build it explicitly and the O(1) story becomes
visible — this is what the visualizer below animates:
// The interview-canonical structure: hash map + doubly-linked list. class Node { constructor(key, val) { this.key = key; this.val = val; this.prev = this.next = null; } } // Two sentinel nodes remove every null-pointer edge case. // head ⇄ [MRU] ⇄ … ⇄ [LRU] ⇄ tail moveToFront(node) { // O(1): just rewire 4 pointers remove(node); // node.prev.next = node.next; … addAfterHead(node); // splice back at the head }
Map finds the node in O(1). Splice it out of the list and re-insert at the head. Return its value. It's now the MRU.
Key not in the map. get returns -1; put falls through to insertion. No list walk either way.
Update the node's value, then move it to the head. Same O(1) splice as a get-hit.
Remove the tail node (the LRU), delete its key from the map, then insert the new node at the head. Two O(1) edits, no scan.
Chapter 03 — Live Visualization
A capacity-3 cache, auto-playing. Watch the doubly-linked list reorder on each access and the tail node dissolve when a new key arrives at capacity.
Chapter 04 — Deep Dive
This is where the 10,000 basic videos stop. Here's what actually matters at scale.
It's tempting to keep a single array ordered by recency. But "move this key to the front" then costs O(n): you must find the key (linear scan, unless you also keep a map) and then shift every element after it to fill the gap. Eviction from the end is cheap, but the move-to-front on every access dominates. The doubly-linked list removes both costs — the map finds the node, and the prev/next pointers relink it with no shifting.
All three are eviction policies that differ only in what they sacrifice:
| Policy | Evicts | Cost |
|---|---|---|
| FIFO | Oldest inserted, ignoring use | Cheapest; ignores access patterns |
| LRU | Least recently accessed | O(1) with map + list; great default |
| LFU | Least frequently accessed | Needs a frequency map + buckets to stay O(1) |
Rule of thumb: LRU wins when recent access predicts future access — which is most real workloads. LFU wins for skewed, stable popularity (a few keys hammered forever), at the cost of more machinery.
Without sentinels, every splice has to special-case "is this the
head?" and "is this the tail?" — a swamp of null checks where
bugs breed. Two permanent dummy nodes (head and
tail) that never hold data mean every real node
always has a non-null prev and next.
Insertion and removal become four unconditional pointer writes.
head.next = tail and tail.prev = head.
Now "move to front" is always addAfter(head, node)
and "evict LRU" is always remove(tail.prev) — no
branches.
The textbook LRU is single-threaded. Under concurrent access, two threads reordering the list at once will corrupt the pointers. In production you either guard the whole structure with a lock (simple, but the lock becomes the bottleneck) or reach for a sharded / lock-striped design, or a library like Caffeine (Java) that approximates LRU with lock-free ring buffers and a background eviction thread.
It's everywhere a fast store has a hard size limit:
| System | What it caches |
|---|---|
| CPU / page cache | Hardware & OS approximate LRU to choose which cache lines or memory pages to evict. |
| Browser cache | Recently visited resources stay; stale ones drop first. |
| DB buffer pool | MySQL/InnoDB keep hot pages in memory using LRU-style lists. |
| Memoization | Python's functools.lru_cache caps memory by evicting least-recently-used results. |
Chapter 05 — Dharma & The Cache
Karma is the principle that every action has a consequence,
deferred to the right time. In an LRU cache, the action is
attention — every
get and put is a touch that earns a key
its place at the head.
// Every access is an act of attention… cache.get('mantra'); // touched → moves to MRU, the living present // …and what attention forgets, the cache releases. cache.put('new', value); // at capacity → the least-touched key dissolves
What you attend to is preserved; what you neglect drifts to the tail and is, in time, released. The cache holds no grudge and plays no favourites — it simply keeps what is alive in the present and lets the rest dissolve. Samhara is not destruction; it is the clearing that makes room for what comes next.
"Do not cling to the least-recently-used. Release it, and the structure stays clear." — the Gita, interpreted for systems engineers 🙏
Chapter 06 — Test Yourself
Three questions. No cheating. Your karma is watching.
node.prev.next — which requires a prev pointer. A singly-linked list would force an O(n) walk to find the predecessor, breaking the whole guarantee.The Enlightenment
कर्म करो, फल की चिंता मत करो