1. Introduction & Motivation
A singly or doubly linked list on its own is a modest tool: fast insertion and deletion at known positions, but slow search, since finding anything requires walking node by node. This lesson looks at two designs that get far more mileage out of that simple structure by combining it with something else, or by adding structure on top of it.
The first, the LRU (Least Recently Used) Cache, pairs a doubly linked list with a hashmap so that both "find a key" and "evict the oldest entry" run in O(1) time — something neither structure achieves alone. The second, the Skip List, stacks several linked lists at increasing "skip" distances on top of a sorted base list, turning an O(n) linear scan into an O(log n) expected search without any of the rotation logic a balanced binary search tree needs. Along the way we also solve Intersection of Two Linked Lists (LeetCode 160), a two-pointer problem that sharpens the pointer-manipulation skills both later designs depend on.
2. Prerequisites
This lesson builds directly on two earlier posts and does not repeat their material:
- The Doubly Linked List section of Part 6: Linked Lists Guide covers node insertion and O(1) deletion given a node reference — exactly the mechanics the LRU Cache's internal list reuses for moving a node to the front and evicting from the back. If
prev/nextpointer surgery is unfamiliar, read that section first. - The Hash Tables section of Part 11 covers average-case O(1) hashmap lookup, insertion, and deletion — the second half of the LRU Cache's combo.
You should also be comfortable with the two-pointer traversal and cycle-detection patterns from the Two Pointer Techniques section of the same guide; the intersection problem below is a direct application of that style of reasoning.
3. Intersection of Two Linked Lists (LeetCode 160)
Given the heads of two singly linked lists that may or may not share a tail, find the node where they intersect, or return nothing if they never merge. Comparing values is not enough — a value can repeat by coincidence. The intersection is defined by shared node identity, the point where two chains of pointers become one.
A hashmap of visited nodes solves this in O(m+n) time and O(m) space. The elegant two-pointer trick solves it in O(m+n) time and O(1) extra space: walk pointer a down list A and pointer b down list B; whenever a pointer falls off the end of its own list, redirect it to the head of the other list. Because pointer a then travels lenA + lenB nodes in total and pointer b travels lenB + lenA nodes, both pointers arrive at the intersection node — if one exists — after exactly the same number of steps, having each walked both lists' unique prefixes exactly once. If there is no intersection, both pointers become None/null at the same step and the loop ends with no match.
Hand trace on two different-length lists
List A (length 4): 1 → 2 → 8 → 9. List B (length 3): 6 → 8 → 9, where the nodes holding 8 and 9 are the same objects in both lists (the true intersection starts at the node holding 8). Pointer a starts at A's head, pointer b starts at B's head. At each step, compare first, then advance each pointer along its own list, or swap to the other list's head once it runs out.
| Step | a (before move) | b (before move) | Same node? | Next move |
|---|---|---|---|---|
| 0 | 1 | 6 | No | a→2, b→8 |
| 1 | 2 | 8 | No | a→8, b→9 |
| 2 | 8 | 9 | No | a→9, b→null |
| 3 | 9 | null | No | a→null, b→headA (1) |
| 4 | null | 1 | No | a→headB (6), b→2 |
| 5 | 6 | 2 | No | a→8, b→8 |
| 6 | 8 | 8 | Yes | loop ends, return node holding 8 |
Swapping heads is what synchronizes the pointers: by the time they might meet, pointer a has walked A's 2 unique nodes plus all of B (3 nodes) plus is now re-walking A, and pointer b has walked B's 1 unique node plus all of A (4 nodes) plus is now re-walking B. Both have covered lenA + lenB = 7 total nodes by step 6, which lands them on the same shared node at the same time — no length pre-computation needed.
4. Core Insight for LRU Cache
get and put by itself.
The fix is to run them side by side over the same set of nodes. The doubly linked list stores nodes in recency order — most recently used near the head, least recently used near the tail — and gives O(1) "move this node to the front" and O(1) "drop the tail node" once you already have a node reference. The hashmap maps each key directly to its node object in that list, so a get(key) or put(key, value) never has to search: the hashmap supplies the node reference in O(1), and the list repositions or evicts that node in O(1). Every node lives in exactly one place in memory but is reachable through both structures.
5. LRU Cache (LeetCode 146)
The implementation below uses a doubly linked list with two dummy sentinel nodes — head and tail — so that every real node always has a non-null prev and next, removing edge cases for the first/last real node. Recency order runs head-side (most recently used) to tail-side (least recently used): inserting a node always places it right after head, and eviction always removes the node right before tail. The hashmap stores key → node.
Tracing the recency order (front = most recently used, back = least recently used) confirms the eviction choices:
| Operation | Order front → back after | Returned |
|---|---|---|
put(1, 1) | [1] | — |
put(2, 2) | [2, 1] | — |
get(1) | [1, 2] | 1 |
put(3, 3) | [3, 1] (evicted 2, the back-most entry) | — |
get(2) | [3, 1] | -1 (not found) |
put(4, 4) | [4, 3] (evicted 1, the back-most entry) | — |
get(1) | [4, 3] | -1 (not found) |
get(3) | [3, 4] | 3 |
get(4) | [4, 3] | 4 |
6. Complexity Analysis for LRU Cache
get and put — a hashmap lookup plus a constant number of pointer updates on the doubly linked list, regardless of how many entries the cache holds. Space: $O(\text{capacity})$ — one node and one hashmap entry per cached key, plus the two dummy sentinels.7. Skip List — Core Insight
Which nodes appear at which levels is decided by a coin flip at insertion time: a new node is promoted to level 1 with probability $p$ (typically $0.5$), promoted again to level 2 with probability $p$ again, and so on, so roughly half as many nodes populate each successive level. This randomization is what gives a Skip List its expected $O(\log n)$ height and search cost without any deterministic rebalancing step — there is no rotation logic and no invariant to repair after every insert or delete, unlike a self-balancing BST. See Part 9: BST, AVL & Red-Black Trees for that deterministic-rebalancing alternative, which trades randomized simplicity for worst-case (rather than expected) logarithmic guarantees.
8. Skip List — Implementation
Each node stores a value and a forward array of next-pointers, one per level the node participates in. The list keeps a header sentinel with pointers at every possible level and tracks the highest level currently in use. search, insert, and delete all share the same top-down descent: at each level, advance while the next node's value is still less than the target, then drop a level.
9. Skip List — Complexity Analysis
search, insert, and delete — each level descent is expected to examine only a small constant number of nodes before dropping down, and there are an expected $O(\log n)$ levels. Space: $O(n)$ expected — every element occupies exactly one level-0 node, and the geometric level-promotion process adds only a small constant-factor overhead of extra forward pointers per node on average (about $\frac{1}{1-p}$ pointers per node for promotion probability $p$).10. Applications & Alternatives
Where these structures show up in real systems
- LRU Cache — OS page replacement: approximations of LRU decide which memory pages to evict when physical RAM is full.
- LRU Cache — browser cache: browsers evict least-recently-viewed cached resources first when local storage limits are hit.
- LRU Cache — database buffer pools: database engines keep recently accessed disk pages in memory using LRU or LRU-like eviction policies.
- Skip List — Redis sorted sets: Redis implements the ZSET data type internally with a Skip List for ordered, O(log n) range and rank operations.
- Skip List — LevelDB / RocksDB memtables: both storage engines use Skip Lists as the in-memory sorted structure that buffers writes before they are flushed to disk.
11. Exercises & Mastery
Easy
- Design a fixed-size circular buffer cache with simple FIFO eviction (oldest inserted entry is evicted first, regardless of access pattern). Contrast it with the LRU Cache above: what workload makes FIFO perform identically to LRU, and what workload makes FIFO perform much worse?
Medium
- LFU Cache (LeetCode 460): design a cache that evicts the least frequently used key instead of the least recently used one, breaking frequency ties by recency. Sketch the data structures needed — a hashmap from key to (value, frequency), a hashmap from frequency to a doubly linked list of keys at that frequency (ordered by recency), and a pointer to the minimum frequency currently in use — so that
getandputboth remain O(1). A brief sketch of the frequency-bucket structure is sufficient; a fully working implementation is not required.
Hard
- Extend the Skip List above to support a range query,
range(low, high), that returns all stored values in[low, high]in sorted order. Use level 0's descent to locatelowin O(log n), then walk level 0 forward, collecting values until exceedinghigh. State the total complexity in terms of n and the number of matching elements k.
Review rhythm: tomorrow, redraw the LRU Cache's doubly linked list plus hashmap from memory and re-derive why each alone is insufficient; next week, implement both the LRU Cache and the Skip List in a language you did not use here; several weeks later, connect the "randomized level promotion" idea in Skip Lists to other randomized algorithms you have studied, such as randomized quicksort.
12. Summary Cheat Sheet
- Intersection of Two Linked Lists: two pointers, each swapping to the other list's head on reaching its own end, meet at the intersection (or both reach null) after lenA + lenB steps — O(m+n) time, O(1) space.
- LRU Cache: doubly linked list (recency order, O(1) reorder/evict) + hashmap (key → node, O(1) lookup) together give O(1)
getandput; neither structure alone achieves that. - LRU Cache complexity: O(1) time for get/put, O(capacity) space.
- Skip List: layered linked lists with randomized level promotion give O(log n) expected search/insert/delete, without the rotation logic of a balanced BST.
- Skip List complexity: O(log n) expected time for all operations, O(n) expected space with a small constant-factor overhead.
- Real systems: LRU underlies OS page replacement, browser caches, and database buffer pools; Skip Lists underlie Redis sorted sets and LSM-tree memtables (LevelDB/RocksDB).