CASE STUDY 02 · ADVANCED DATA STRUCTURES

Advanced Hash Table + Doubly Linked List

Least Recently Used Cache

Combine constant-time lookup with constant-time recency updates, then evict the correct item when limited memory becomes full.

01 · PROBLEM DEFINITION

Limited fast memory needs an eviction policy

A cache keeps frequently needed data close to the application so repeated requests avoid an expensive database, disk or network operation. Capacity is limited, so inserting a new key into a full cache requires removing an existing entry. Least Recently Used (LRU) removes the entry that has gone unused for the longest time.

“Recently used” includes both reading a key and updating it. Therefore every successful get and put changes recency order. A miss does not. The system must support lookup, insertion, update and eviction efficiently while keeping the recency policy correct after every mutation.

Get

Return a stored value and move its entry to most-recent position.

Put

Insert or update, then mark the key most recently used.

Evict

When full, remove exactly the least-recent entry.

Example use: A course portal may cache recently opened lesson records. A repeated lesson request becomes a hit; a missing lesson is fetched from slower storage and then cached.
02 · REQUIREMENTS & POLICY

Make recency semantics unambiguous

OperationExisting keyMissing keyRecency effect
GET(key)Return value; hit++Report miss; miss++Hit moves key to MRU; miss changes nothing
PUT(key,value)Replace valueCreate entry; evict first if fullStored key becomes MRU
REMOVE(key)Delete from both structuresReport absentRemaining order is preserved
DISPLAYShow entries from MRU to LRUNo change

Defined terms

MRU

Most Recently Used: the key touched most recently. It lives at the list head.

LRU

Least Recently Used: the oldest untouched key. It lives at the list tail and is the eviction candidate.

Policy detail: Updating an existing value counts as use. The key must move to MRU even when no new node is allocated.
03 · HYBRID DATA MODEL

One structure is not enough for both goals

A hash table finds a key in O(1) average time but does not naturally identify the oldest key. A linked list orders entries by recency, but finding an arbitrary key would take O(n). The LRU cache combines them: the hash table points directly to the same nodes that also form a doubly linked list.

Hash bucket 4 ───────┐
Hash bucket 11 ──┐  │       recency links
                 ▼  ▼
MRU/head → [30:300] ⇄ [10:100] ⇄ [20:200] ← LRU/tail

Node fields

FieldPurposeUsed by
key, valueCached mappingBoth views
previous, nextBidirectional recency orderDoubly linked list
hashNextCollision chain inside one bucketHash table

The cache owns each node exactly once even though two structures reference it. The hash table and list are two indexes over the same allocation; neither creates a duplicate node.

04 · CORRECTNESS INVARIANTS

Every operation must preserve both views

  1. size equals the number of unique nodes reachable from the recency list.
  2. Every list node appears in exactly one hash bucket chain.
  3. Every hash-table node appears exactly once in the recency list.
  4. mostRecent->previous == NULL and leastRecent->next == NULL when non-empty.
  5. For adjacent nodes A.next == B, the reverse link satisfies B.previous == A.
  6. An empty cache has size zero and both endpoints NULL.
  7. Size never exceeds capacity.
Atomic reasoning: removing a node means unlinking it from the recency list and its hash bucket before freeing it. Leaving either reference behind creates corruption or use-after-free.

Ownership lifecycle

malloc on new PUT
Hash table + list own node
Move without allocation
Evict/remove/clear
free exactly once
05 · OPERATION DESIGN

Small pointer functions reduce mutation risk

Mark a node most recent

  1. If it is already the head, do nothing.
  2. Detach it by reconnecting its previous and next neighbours.
  3. If it was the tail, move the tail pointer to its previous node.
  4. Attach it before the old head.
  5. Set it as the new head.

GET

Hash key
Search bucket chain
Hit: move to MRU
Return value

On a miss, increment miss count and leave the linked list untouched.

PUT

  1. Search for the key.
  2. If found, update its value and move it to MRU.
  3. If absent and full, identify the tail in O(1), remove it from both indexes and free it.
  4. Allocate one new node, insert it into its hash bucket and attach it as MRU.

Why a doubly linked list?

A hash lookup returns the exact node to move. With both previous and next, it can be detached in O(1). A singly linked list would still need the predecessor, forcing a scan or an additional mapping.

06 · COMPLETE IMPLEMENTATION

Compiler-ready C11 simulator

The program stores integer keys and values, uses separate chaining for hash collisions, displays MRU-to-LRU order and tracks hit/miss statistics. Its small capacity limit keeps classroom traces readable; the algorithm itself is not tied to that limit.

programs/lru-cache.c
Open Compiler
Loading source…

Hash collision

hashNext chains keys that map to the same bucket.

Recency mutation

detach and attach handle head, middle, tail and singleton nodes.

Cleanup

clearCache traverses the list once and frees every allocation.

07 · INTERACTIVE PROGRAM TRACING

Trace a capacity-three cache

  1. Create an empty cache.
  2. Store the first key.
  3. Key 2 becomes MRU.
  4. Store the third key.
  5. Reading key 1 refreshes its recency.
  6. A missing key does not change order.
  7. A full-cache insertion begins eviction.
  8. The new key becomes MRU.
  9. Updating a key counts as use.
  10. Explicit removal preserves remaining order.
Current state

Press Next to begin.

08 · TEST STRATEGY

Test every position and both indexes

Test 1 — Capacity one
PUT 1, then PUT 2. Key 1 must be evicted, key 2 must be both MRU and LRU, and size must remain one.
Test 2 — GET refreshes recency
With order [3,2,1], GET 1 must produce [1,3,2]. The next insertion must evict 2, not 1.
Test 3 — Update without growth
PUT an existing key with a new value. Size must not change, the node becomes MRU and a later GET returns the new value.
Test 4 — Collision safety
Insert different keys that map to one bucket. Each GET must find the correct node; removing one must not disconnect the others.
Test 5 — Remove head, middle and tail
Delete each position in separate runs. Check endpoint pointers, reverse links, size and hash lookup after every deletion.
Test 6 — Miss behaviour
GET an absent key. Miss count increases, but list order and size remain identical.
Test 7 — Complete cleanup
Exit with a full cache and use a memory checker. Every node must be freed once with no invalid reads.
09 · COMPLEXITY & TRADE-OFFS

O(1) is average—not unconditional

OperationAverage timeWorst timeExtra space
GETO(1)O(n)O(1)
PUT existingO(1)O(n)O(1)
PUT new/evictO(1)O(n)O(1) per node
REMOVEO(1)O(n)O(1)
DISPLAY/CLEARO(n)O(n)O(1)

The worst case occurs when many keys collide into one bucket chain. A production cache resizes its table or uses a robust map implementation to keep load factor controlled. Total storage is O(capacity + bucket count).

When LRU is imperfect

LRU assumes recent past predicts near future. A one-time sequential scan larger than the cache can evict genuinely useful entries—a behaviour called cache pollution. LFU considers frequency; TTL policies consider age; ARC and related strategies adapt to workload. Policy selection depends on access patterns, not fashion.

10 · PRACTICE & EXTENSIONS

Check the hybrid design

Why does an efficient LRU cache combine two structures?

After GET succeeds, what must happen?

Build the next version

  1. Resize the hash table when load factor crosses a chosen threshold.
  2. Add string keys and dynamically owned string values.
  3. Add a TTL and distinguish capacity eviction from expiry.
  4. Persist hit/miss/eviction statistics and compare workloads.
  5. Design thread-safe GET and PUT operations; explain locking granularity.
11 · INTERVIEW PREPARATION

Defend every structural decision

Why not use only a hash table?

A hash table locates keys quickly but does not maintain a constant-time oldest-entry order. Finding the least recent entry would require extra metadata and a scan.

Why not use only a doubly linked list?

It gives O(1) removal when a node is already known, but locating a key is O(n). The hash table provides the missing direct lookup.

Why is the tail the eviction victim?

The list is maintained from most recent at head to least recent at tail. Therefore the tail encodes exactly the LRU policy and can be removed without scanning.

What is the hardest correctness problem?

Keeping the hash table and recency list consistent. Every insertion and deletion must update both views; moving a node changes only list links, not its hash membership.

How would concurrency change the design?

GET is no longer read-only because it mutates recency. Concurrent operations need synchronization around both indexes, careful lock ordering, and a policy for reducing contention.

12 · KEY TAKEAWAY

Powerful structures are often compositions

The LRU cache achieves its performance by assigning separate responsibilities: the hash table answers “where is this key?” and the doubly linked list answers “which key is oldest?” Correctness comes from preserving the relationship between those views after every operation.