Linked List
Scope — Pointer surgery on singly and doubly linked lists — reversal, merging, reordering, dummy-head technique, and cycle handling. See also: linked_list_examples.md — the worked solutions these templates are for; 2_pointers_linkedlist.md — the fast/slow pointer specialisation; design.md — LRU and other list+map designs; heap.md — k-way list merging; recursion.md — recursive list rewriting.
LeetCode Problem Lists
Time Complexity
| Data structure | Search | Insert | Delete | Min/Max |
|---|---|---|---|---|
| Linked List | O(n) | O(1) | O(1) | O(n) |
Insert / Delete are O(1) given the target node (e.g. head, or a node you already hold); locating that node first is O(n).
0) Concept
- fucking algorithm : reverse part of linked list
- fucking algorithm : reverse k set of linked list
- fucking algorithm : check palindrome linked list
Recognise — the input is a ListNode head, and the answer is a re-wired list or one
node of it. The follow-up is almost always “in O(1) extra memory”, which rules out copying
the values into an array.
Key point — a singly linked node only knows what comes after it. To delete or insert at
a node you must be standing on the node before it (cur.next = cur.next.next), so every
technique below is a way of making sure you hold the right handle when the surgery happens.
# python
# Definition for singly-linked list.
class ListNode(object):
def __init__(self, val=0, next=None):
self.val = val
self.next = next
// java
// Single Linkedlist
public class ListNode{
// attr
public int val;
public ListNode next;
// constructor
public ListNode(){
}
public ListNode(int val){
this.val = val;
}
ListNode(int val, ListNode next){
this.val = val;
this.next = next;
}
}
// init a ListNode
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
// motify node's value
node1.val = 0;
// connect nodes
node1.next = node2;
node2.next = node3;
// java
// Double linked list
// LC 146
public class Node {
int key;
int val;
Node prev;
Node next;
public Node(int key, int val) {
this.key = key;
this.val = val;
this.prev = null;
this.next = null;
}
}
0-1) Types
- Linked list
- Cycle linked list
- Bi-direction linked list
- Double Linked list
- LC 146, LC 460
- Others
- LC 138 :
pythondic = dict() m = n = head dic[m] = Node(m.val)- LC 208 :
- trie
pythonself.children = defaultdict(Node) - problem types
- reverse
- reverse linked list
- LC 206
- reverse linked list within start, end point
- LC 92, LC 25
- reverse part of linked list
- reverse k set of linked list
- reverse linked list
- merge
- merge 2 linked list
- check
- check cyclic linked list
- check beginning of cyclic linked list
- remove N th node
- Remove Nth Node From End of List - LC 19
- combinations
- combinations of above cases
- reverse
Recognition
| Signal in the statement | Constraint cue | Reach for | Not this if |
|---|---|---|---|
| “reverse the linked list” | O(1) extra memory | Template 1 iterative reversal | only a range or every k nodes is reversed → Template 4 |
| “remove all nodes with value …”, “delete duplicates”, the head may be deleted | one pass | Template 2 dummy head | you are handed only the node to delete, not the head (LC 237) → copy the next value in |
| “all nodes less than x come before …”, “odd indices then even”, “preserve the relative order” | O(1) extra memory | Template 3 two dummies | order inside a group may change → an array partition is enough |
| “reverse nodes from position left to right”, “in groups of k”, “swap every two adjacent nodes” | one pass, O(1) memory | Template 4 segment reversal | the whole list is reversed → Template 1 |
| “middle node”, “n-th node from the end”, “split in half”, “reorder”, “palindrome” | one pass, length unknown | Template 5 fast/slow | “does it cycle / where does the cycle start” → 2_pointers_linkedlist.md |
| “digits stored in reverse order”, “add the two numbers” | lists up to 100 nodes, so no fixed-width integer fits | Template 6 carry loop | most significant digit first (LC 445) → reverse both first, or use two stacks |
| “design a cache”, “get and put must run in O(1)”, “evict the least recently used” | O(1) per operation | Template 7 hash map + doubly linked list | evict by frequency (LC 460) → the LFU variant |
1) General form
Every template below runs on the ListNode defined above. They are ordered the way an
interview builds on them: reversal first, then the dummy head, then the techniques that
combine the two, and the cache designs last.
Template 1: Iterative Reversal — LC 206 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “reverse a singly linked list”, or any problem whose plan contains “reverse the second half” (LC 143, LC 234). Follow-up: O(1) extra memory, so iterate rather than recurse.
Key point — prev is always the head of the already-reversed part and curr the head of
the untouched part; caching curr.next before the flip is what keeps the untouched part
reachable.
# python
# LC 206 - Reverse Linked List
# IDEA: walk once; at each node cache next, flip the link back to prev, advance both
# time = O(n), space = O(1)
class Solution(object):
def reverseList(self, head):
prev = None # head of the reversed part (empty at first)
curr = head # head of the part not yet reversed
while curr:
nxt = curr.next # 1) cache next: the flip below overwrites it
curr.next = prev # 2) flip the link
prev = curr # 3) advance prev
curr = nxt # 4) advance curr
return prev # curr is None, so prev is the old tail = new head
// java
// LC 206 - Reverse Linked List
// IDEA: walk once; at each node cache next, flip the link back to prev, advance both
// time = O(n), space = O(1)
public ListNode reverseList(ListNode head) {
ListNode prev = null; // head of the reversed part
ListNode curr = head; // head of the part not yet reversed
while (curr != null) {
ListNode next = curr.next; // 1) cache next
curr.next = prev; // 2) flip the link
prev = curr; // 3) advance prev
curr = next; // 4) advance curr
}
return prev; // the old tail is the new head
}
Complexity
- Time O(n): the
while currloop visits each node once and does four assignments there. - Space O(1): only
prev,currandnxt; the nodes are re-linked, never copied.
Dry run
1 -> 2 -> 3
step prev curr reversed part
0 None 1 (empty)
1 1 2 1
2 2 -> 1 3 2 -> 1
3 3 -> 2 -> 1 None return prev
Traps
curr.next = prevbeforenxt = curr.nextloses the rest of the list:1 -> 2 -> 3returns1.- Returning
headinstead ofprev:headis now the tail, so[1,2,3]returns[1]. - No special case is needed for
[]or[1]: the loop runs zero or one times andprevis right.
Not this if — only part of the list is reversed (a range, every k nodes, pairs) → the segment reversal in Template 4, which is this loop run k times plus two reconnections.
Drill — LC 206 ⭐ · LC 234 (reverse the back half, compare) · LC 143 ⭐ (middle, reverse, interleave) · LC 92 (a range: Template 4)
Recursive form — O(n) stack
The same answer, top-down: reverse everything after head, then hang head after the node
that used to follow it. It costs a stack frame per node, so it is the second answer, not the first.
# python
# LC 206 - Reverse Linked List (recursive)
# IDEA: reverse the rest first; head.next is then the rest's tail, so hang head after it
# time = O(n), space = O(n) call stack
class Solution(object):
def reverseList(self, head):
if head is None or head.next is None: # empty, or the last node: already reversed
return head
new_head = self.reverseList(head.next) # the original last node, passed up unchanged
head.next.next = head # the node after head now points back at it
head.next = None # cut the old forward link, or 2 nodes form a cycle
return new_head
// java
// LC 206 - Reverse Linked List (recursive)
// IDEA: recurse to the tail, then let each frame flip the link BEHIND it on the way back.
// `newHead` is the original last node and is passed up unchanged.
// time = O(n), space = O(n) (call stack)
public ListNode reverseList(ListNode head) {
// base case: empty list, or already at the last node
if (head == null || head.next == null) {
return head;
}
// reverse everything after `head`; newHead is the tail of the ORIGINAL list
ListNode newHead = reverseList(head.next);
// head.next now sits BEHIND head in the reversed list: point it back at head
head.next.next = head;
// cut the old forward link, or the two nodes form a 2-cycle
head.next = null;
return newHead;
}
Template 2: Dummy Head Deletion — LC 203 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “remove every node with value …”, “delete duplicates”, “remove the n-th node”,
or any edit where the head itself may be removed or replaced. Without a dummy, each of
these needs an if head is the target branch.
Key point — the dummy gives curr a standing position before the head, and curr only
ever looks at curr.next. After a deletion curr stays put, so a run of matches ([6,6,6])
is removed one link at a time without skipping any.
# python
# LC 203 - Remove Linked List Elements
# IDEA: dummy before head; look ahead at curr.next; on a match skip it and do NOT advance
# time = O(n), space = O(1)
class Solution(object):
def removeElements(self, head, val):
dummy = ListNode(0)
dummy.next = head
curr = dummy # always the last node known to be kept
while curr.next:
if curr.next.val == val:
curr.next = curr.next.next # skip the match; curr stays, the new next is unchecked
else:
curr = curr.next # next is kept, so move onto it
return dummy.next # the head may have been removed
// java
// LC 203 - Remove Linked List Elements
// IDEA: dummy before head; look ahead at curr.next; on a match skip it and do NOT advance
// time = O(n), space = O(1)
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode curr = dummy; // always the last node known to be kept
while (curr.next != null) {
if (curr.next.val == val) {
curr.next = curr.next.next; // skip the match; curr does not move
} else {
curr = curr.next; // no match: move forward
}
}
return dummy.next;
}
Complexity
- Time O(n): every iteration of
while curr.nexteither removes a node or advancescurr, so there are at most n iterations in total. - Space O(1): one dummy node and one cursor.
Dry run ([6,6,6,3], val=6)
Initial: dummy -> 6 -> 6 -> 6 -> 3, curr at dummy
Step 1: curr.next.val = 6 (match!)
Action: curr.next = curr.next.next
Result: dummy -> 6 -> 6 -> 3 (curr STAYS at dummy)
Step 2: curr.next.val = 6 (match!)
Action: curr.next = curr.next.next
Result: dummy -> 6 -> 3 (curr STAYS at dummy)
Step 3: curr.next.val = 6 (match!)
Action: curr.next = curr.next.next
Result: dummy -> 3 (curr STAYS at dummy)
Step 4: curr.next.val = 3 (no match)
Action: curr = curr.next
Result: curr moves to node 3
Step 5: curr.next = null, exit loop
Return: dummy.next = [3]
Traps
- Advancing
currafter a deletion skips the node that slid intocurr.next:[6,6,3], val=6, returns[6,3]. - Returning
headinstead ofdummy.next:[7,7,7], val=7, returns the deleted7instead of[]. - Testing
curr.valinstead ofcurr.next.val: by the time you see the match you are standing on it, and a singly linked node cannot reach the node before it.
Not this if — you are handed only the node to delete, never the head (LC 237): there is no
prev to stand on, so copy node.next.val into it and delete the next node instead.
Drill — LC 203 · LC 83 (compare adjacent values) · LC 19 ⭐ (a gap of n) · LC 82 (drop every copy) · LC 1474
Worked: LC 19 and the other Template 2 problems in linked_list_examples; LC 82 in 2_pointers_linkedlist § 2-2.
Template 3: Two Dummies, Split then Join — LC 86 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — every node goes into one of two groups and each group must keep its original order: “all nodes less than x come before nodes greater than or equal to x … preserve the relative order” (LC 86), “group the odd indices together followed by the even” (LC 328).
Key point — each chain has a dummy (a fixed handle on its head) and a tail pointer; every
node is appended to exactly one tail, so after one pass p1.next = dummy_2.next joins them
without ever tracking chain 2’s first node.
| Handle | What it is | Needed for |
|---|---|---|
dummy_1 |
sits before the head of chain 1 | the answer — return dummy_1.next |
p1 |
the tail of chain 1 | the join: chain 1’s tail must point at chain 2’s head |
dummy_2 |
sits before the head of chain 2 | the join: dummy_2.next is chain 2’s head |
p2 |
the tail of chain 2 | termination: p2.next = None |
# python
# LC 86 - Partition List
# IDEA: two dummies -> append each node to the tail of its chain, then
# terminate chain 2 and hang it off chain 1's tail via dummy_2.next
# time = O(n), space = O(1)
class Solution(object):
def partition(self, head, x):
dummy_1 = ListNode(0) # chain 1: values < x
dummy_2 = ListNode(0) # chain 2: values >= x
p1 = dummy_1 # tail of chain 1
p2 = dummy_2 # tail of chain 2
curr = head
while curr:
if curr.val < x:
p1.next = curr
p1 = p1.next
else:
p2.next = curr
p2 = p2.next
curr = curr.next
# terminate chain 2: p2 may still point at a node now in chain 1
p2.next = None
# join: dummy_2.next is chain 2's head, so it never has to be tracked
p1.next = dummy_2.next
return dummy_1.next
// java
// LC 86 - Partition List
// IDEA: two dummies, one tail per chain; cut chain 2's tail, then join chain 1's tail to it
// time = O(n), space = O(1)
public ListNode partition(ListNode head, int x) {
ListNode dummy1 = new ListNode(0); // chain 1: values < x
ListNode dummy2 = new ListNode(0); // chain 2: values >= x
ListNode p1 = dummy1;
ListNode p2 = dummy2;
ListNode curr = head;
while (curr != null) {
if (curr.val < x) {
p1.next = curr;
p1 = p1.next;
} else {
p2.next = curr;
p2 = p2.next;
}
curr = curr.next;
}
p2.next = null; // terminate chain 2, or the result can loop
p1.next = dummy2.next; // join: chain 1's tail -> chain 2's head
return dummy1.next;
}
Complexity
- Time O(n): the single
while currpass appends each node to one tail; the join is O(1). - Space O(1): the nodes are re-linked, never copied; only the two dummies are new.
Dry run ([1,4,3,2,5,2], x = 3)
node goes to chain 1 (< 3) chain 2 (>= 3)
1 chain 1 d1 -> 1 d2
4 chain 2 d1 -> 1 d2 -> 4
3 chain 2 d1 -> 1 d2 -> 4 -> 3
2 chain 1 d1 -> 1 -> 2 d2 -> 4 -> 3
5 chain 2 d1 -> 1 -> 2 d2 -> 4 -> 3 -> 5
2 chain 1 d1 -> 1 -> 2 -> 2 d2 -> 4 -> 3 -> 5
^p1 ^p2
p2.next = None 5.next was still the last 2 -> cut it
p1.next = d2.next 2 -> 4
d1 -> 1 -> 2 -> 2 -> 4 -> 3 -> 5 return d1.next => [1,2,2,4,3,5]
Traps
- Forgetting
p2.next = Nonemakes a cycle. The nodes are re-linked, not copied, so chain 2’s last node still carries its oldnext. In the trace that is5 -> 2, and the final 2 sits in chain 1 before 5 — a loop. Only chain 2 needs the cut: chain 1’s tail is about to be overwritten by the join anyway. - Join with
dummy_2.next, notdummy_2. The dummy is a placeholder value0; hanging the dummy itself offp1inserts a phantom node. - Empty groups need no guard. All nodes
< x:dummy_2.nextisNone, and the join just re-terminates chain 1. All nodes>= x:p1is stilldummy_1, so the join makesdummy_1.nextchain 2’s head. Empty input returnsNonethe same way.
Not this if — the order inside a group may change, or the input is an array: LC 75’s in-place Dutch-flag swap is cheaper there, but it is not stable, and LC 86 requires the original relative order within each group.
| # | Problem | The predicate that picks the chain |
|---|---|---|
| 86 | Partition List | node.val < x — the template above |
| 328 | Odd Even Linked List | the node’s index is even — the same two dummies, p2.next = None, p1.next = dummy_2.next |
| 2161 | Partition Array According to Given Pivot | the array form: three buckets (<, ==, >), concatenated in order |
| 21 | Merge Two Sorted Lists | the reverse move — one dummy, two inputs, one chain out |
Drill — LC 86 · LC 21 ⭐ (one dummy, two inputs) · LC 328 (split by index) · LC 725 (k chains)
Template 4: Reverse a Segment, Reconnect Both Ends — LC 92 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “reverse the nodes from position left to right” (LC 92), “reverse the nodes k at a time” (LC 25), “swap every two adjacent nodes” (LC 24). Follow-up: one pass, O(1) memory.
Key point — reversing k nodes from start leaves three handles: the new front (the k-th
node), the new tail (start itself) and the first node after the segment. Reconnecting is
always prev.next = new_head and new_tail.next = next_node; the problems differ only in
where the segment is.
| Returned | What it is | Used to reconnect |
|---|---|---|
prev (new_head) |
new head of the reversed chunk | prev_of_segment.next = new_head |
head (new_tail) |
new tail (the original first node) | new_tail.next = next_node |
curr (next_node) |
first node after the segment | the tail must point here |
# python
# LC 92 - Reverse Linked List II
# IDEA: walk prev to the node before `left`, reverse right-left+1 nodes with the helper,
# then reconnect both boundaries with the three handles it returns
# time = O(n), space = O(1)
class Solution(object):
def reverseBetween(self, head, left, right):
dummy = ListNode(0)
dummy.next = head
# 1) walk `prev` to the node BEFORE position `left`
prev = dummy
for _ in range(left - 1):
prev = prev.next
# 2) reverse (right - left + 1) nodes starting at prev.next
new_head, new_tail, next_node = self.reverse_helper(prev.next, right - left + 1)
# 3) reconnect both boundaries
prev.next = new_head # front: prev -> new head of reversed chunk
new_tail.next = next_node # back: old head (now tail) -> rest of list
return dummy.next
def reverse_helper(self, head, k):
"""reverse k nodes from head; return (new_head, new_tail, first node after)"""
prev = None
curr = head
while curr and k > 0:
nxt = curr.next # 1) cache next
curr.next = prev # 2) reverse the link
prev = curr # 3) advance prev
curr = nxt # 4) advance curr
k -= 1
return prev, head, curr
// java
// LC 92 - Reverse Linked List II
// IDEA: walk prev to the node before `left`, reverse right-left+1 nodes in place,
// then reconnect: prev -> new front, old start (now the tail) -> the node after
// time = O(n), space = O(1)
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
for (int i = 0; i < left - 1; i++) {
prev = prev.next; // the node BEFORE the segment
}
ListNode start = prev.next; // becomes the segment's tail
ListNode reversed = null; // front of the reversed segment
ListNode curr = start;
for (int i = 0; i < right - left + 1; i++) {
ListNode next = curr.next;
curr.next = reversed;
reversed = curr;
curr = next;
}
// curr is now the first node AFTER the segment
prev.next = reversed; // front
start.next = curr; // back
return dummy.next;
}
Complexity
- Time O(n): the walk to
leftand the k-step reversal together touch each node at most once. - Space O(1): a fixed number of pointers; the helper is a loop, not recursion.
Dry run ([1,2,3,4,5], left=2, right=4 → reverse 3 nodes 2,3,4)
dummy -> 1 -> 2 -> 3 -> 4 -> 5
└──── reverse these 3 ────┘
Step 1) walk prev (left-1 = 1 step) to node before segment
dummy -> 1 -> 2 -> 3 -> 4 -> 5
^prev ^start
(start = prev.next = node 2)
Step 2) reverse_helper(start=2, k=3)
-- reverses links of 2,3,4 in isolation --
before: 2 -> 3 -> 4 -> 5
after: 2 <- 3 <- 4 5
| |
new_tail new_head
returns:
new_head = 4 (was k-th node, now front of chunk)
new_tail = 2 (was `start`, now points nowhere yet)
next_node = 5 (first node after the reversed part)
Step 3) reconnect boundaries
(C1) prev.next = new_head
node1.next -> 4
(C2) new_tail.next = next_node
node2.next -> 5
Final:
dummy -> 1 -> 4 -> 3 -> 2 -> 5
└── reversed ──┘
return dummy.next => [1, 4, 3, 2, 5] ✓
Traps
- Walking
prevfromheadinstead ofdummy: withleft = 1there is no node before the segment, so[1,2,3], left=1, right=2 cannot reconnect its front. - Forgetting
new_tail.next = next_nodeloses the tail5: the helper already set the old start’snexttoNone, so the result is1 -> 4 -> 3 -> 2 -> None, not a cycle. - In LC 25, calling the helper on a final group shorter than k reverses it too; count k nodes
first (
has_k) and leave a short tail as it is.
Not this if — the whole list is reversed → Template 1; the list is rotated (LC 61), not reversed → find the new tail with a gap and re-stitch, no reversal at all.
Drill — LC 92 · LC 206 ⭐ (k = length) · LC 24 (k = 2, unrolled) · LC 25 (loop the helper) · LC 61
Worked: LC 24, LC 25 and LC 430 in linked_list_examples.
Recursive form — reverse the first n, then a range
The recursive version of the same segment reversal. reverseN reverses the first n nodes
and remembers the node after them in successor; reverseBetween walks left down to 1 and
then calls it. It is O(n) stack, so it is the second answer.
// java
// LC 92 - Reverse Linked List II (recursive)
// IDEA: reverseN(head, n) reverses the first n nodes and reattaches `successor` (node n + 1);
// reverseBetween shifts the window one node per call until it starts at position 1
// time = O(n), space = O(n) (call stack)
ListNode successor = null; // node n + 1, recorded by the base case
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next; // the first node after the reversed part
return head;
}
ListNode last = reverseN(head.next, n - 1);
head.next.next = head; // the node after head now points back at it
head.next = successor; // head is the new tail: reattach the rest
return last;
}
ListNode reverseBetween(ListNode head, int m, int n) {
if (m == 1) {
return reverseN(head, n);
}
// for head.next the range is [m - 1, n - 1]
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
Template 5: Fast/Slow Middle — LC 876 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “return the middle node”, “split the list into two halves” (LC 143, LC 148, LC 234), “n-th node from the end” — anything that needs a position measured from the end or the middle of a list whose length is not given.
Key point — fast moves two nodes for each one slow moves, so when fast cannot take
another full step, slow has covered half the distance: it stands on the middle.
# python
# LC 876 - Middle of the Linked List
# IDEA: fast takes two steps per slow step; when fast runs out, slow is at the middle
# time = O(n), space = O(1)
class Solution(object):
def middleNode(self, head):
slow = head
fast = head
while fast and fast.next: # fast can take two more steps
slow = slow.next
fast = fast.next.next
return slow # even length: the SECOND of the two middles
// java
// LC 876 - Middle of the Linked List
// IDEA: fast takes two steps per slow step; when fast runs out, slow is at the middle
// time = O(n), space = O(1)
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // even length: the second middle
}
Complexity
- Time O(n):
fastreaches the end in n / 2 iterations of thewhile. - Space O(1): two pointers.
Dry run
[1,2,3,4,5] [1,2,3,4,5,6]
slow fast slow fast
1 1 1 1
2 3 2 3
3 5 stop 3 5
4 None stop
return 3 return 4 (the second middle)
Traps
while fast.next and fastdereferencesNoneon an even-length list; testfastfirst.- To split a list for merge sort (LC 148) or reorder (LC 143) you need the first middle, so
the left half is never longer: loop on
while fast.next and fast.next.next. With the second-middle form,[1,2]never splits and merge sort recurses forever.
Not this if — “does the list have a cycle / where does it start” (LC 141, LC 142) uses the same two speeds but a different stopping rule → 2_pointers_linkedlist.md; “n-th from the end” uses equal speeds with a gap of n → the LC 19 card.
Drill — LC 876 · LC 141 ⭐ (cycle: same speeds, stop when they meet) · LC 234 (middle + reverse) · LC 143 ⭐ (first middle, reverse, interleave) · LC 148 (first middle, merge sort)
Template 6: Carry Loop over a Dummy — LC 2 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
Recognise — “the digits are stored in reverse order and each node contains a single digit; add the two numbers”. The lists can be 100 nodes long, so the numbers do not fit in any fixed-width integer: add digit by digit.
Key point — each step writes (a + b + carry) % 10 and carries // 10; the loop runs while
either list or the carry is non-empty, because the carry can outlive both inputs.
# python
# LC 2 - Add Two Numbers
# IDEA: walk both lists at once, append the digit sum mod 10 to a dummy-headed result, carry the rest
# time = O(max(m, n)), space = O(1) besides the output
class Solution(object):
def addTwoNumbers(self, l1, l2):
dummy = ListNode(0)
tail = dummy # last node of the result
carry = 0
while l1 or l2 or carry: # the carry can outlive both lists
total = carry
if l1:
total += l1.val
l1 = l1.next
if l2:
total += l2.val
l2 = l2.next
carry = total // 10
tail.next = ListNode(total % 10)
tail = tail.next
return dummy.next
// java
// LC 2 - Add Two Numbers
// IDEA: walk both lists at once, append the digit sum mod 10 to a dummy-headed result, carry the rest
// time = O(max(m, n)), space = O(1) besides the output
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int total = carry;
if (l1 != null) {
total += l1.val;
l1 = l1.next;
}
if (l2 != null) {
total += l2.val;
l2 = l2.next;
}
carry = total / 10;
tail.next = new ListNode(total % 10);
tail = tail.next;
}
return dummy.next;
}
Complexity
- Time O(max(m, n)): the
whiletakes one digit from each list per step, plus at most one step for a final carry. - Space O(1) besides the result list, which has max(m, n) + 1 nodes at most.
Traps
- Looping on
while l1 and l2drops the longer list’s tail:[9,9,9]+[1]returns[0]. - Looping on
while l1 or l2drops a final carry:[5]+[5]returns[0]instead of[0,1]. - Converting each list to an integer works in Python but overflows
longin Java on a 100-digit input; it is not the answer an interviewer is looking for.
Not this if — the most significant digit comes first (LC 445): reverse both lists with Template 1 and reverse the result back, or push the digits onto two stacks and build the result by prepending.
Drill — LC 2 · LC 369 (add one) · LC 445 (most significant first)
Template 7: Hash Map + Doubly Linked List (LRU) — LC 146 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “design a cache”, “get and put must each run in O(1) average time”,
“evict the least recently used key”. Any structure that needs both O(1) lookup by key and an
O(1)-maintained order.
Key point — the map takes a key to its node, and the node, being doubly linked, can unlink
itself in O(1). Recency is the list order: the most recent node sits just before tail, so
the victim is always head.next.
head(dummy) <-> [LRU] <-> ... <-> [MRU] <-> tail(dummy)
Key helper operations — the whole class is these two, called in pairs:
remove(node)— splice a node out of the list in O(1); needs only the node, because it carries both neighboursadd_to_tail(node)— insert a node just before the dummy tail (MRU position) in O(1)- “touch” a key =
remove(node)thenadd_to_tail(node)— used bygetand byputon an existing key
# python
# LC 146 - LRU Cache
# IDEA: map key -> node for O(1) lookup; the doubly linked list holds recency order,
# MRU just before `tail`, LRU just after `head`. put inserts first, then evicts
# `head.next` if the map has grown past capacity.
# time = O(1) per get/put, space = O(capacity)
class Node:
def __init__(self, key=0, val=0):
self.key = key # kept so eviction can delete the map entry
self.val = val
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.kv_map = {} # key -> Node
# sentinel boundaries: head <-> ... <-> tail
self.head = Node() # LRU side
self.tail = Node() # MRU side
self.head.next = self.tail
self.tail.prev = self.head
def remove(self, node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def add_to_tail(self, node): # insert just before tail (MRU)
prev_node = self.tail.prev
prev_node.next = node
node.prev = prev_node
node.next = self.tail
self.tail.prev = node
def get(self, key):
if key not in self.kv_map:
return -1
node = self.kv_map[key]
self.remove(node)
self.add_to_tail(node) # move to MRU
return node.val
def put(self, key, value):
# case 1) key exists: update in place, refresh to MRU — never evicts
if key in self.kv_map:
node = self.kv_map[key]
node.val = value
self.remove(node)
self.add_to_tail(node)
return
# case 2) new key: insert first ...
node = Node(key, value)
self.kv_map[key] = node
self.add_to_tail(node)
# ... then evict the LRU (the first REAL node, not the dummy) if over capacity
if len(self.kv_map) > self.capacity:
lru_node = self.head.next
self.remove(lru_node)
del self.kv_map[lru_node.key]
// java
// LC 146 - LRU Cache
// IDEA: map key -> node; a sentinel doubly linked list keeps recency order (MRU before tail).
// put inserts first, then evicts head.next if the map has grown past capacity.
// time = O(1) per get/put, space = O(capacity)
class LRUCache {
private static class Node {
int key;
int val;
Node prev;
Node next;
Node(int key, int val) {
this.key = key; // kept so eviction can delete the map entry
this.val = val;
}
}
private final int capacity;
private final Map<Integer, Node> map = new HashMap<>();
private final Node head = new Node(0, 0); // LRU side
private final Node tail = new Node(0, 0); // MRU side
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addToTail(Node node) { // insert just before tail (MRU)
node.prev = tail.prev;
node.next = tail;
tail.prev.next = node;
tail.prev = node;
}
public int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
}
remove(node);
addToTail(node); // move to MRU
return node.val;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) { // existing key: update and refresh, never evict
node.val = value;
remove(node);
addToTail(node);
return;
}
node = new Node(key, value);
map.put(key, node);
addToTail(node);
if (map.size() > capacity) { // evict the first REAL node
Node lru = head.next;
remove(lru);
map.remove(lru.key);
}
}
}
Second solution — the library form. Python’s OrderedDict and Java’s LinkedHashMap are
a hash map over a doubly linked list. Name this first in the interview. Most interviewers then
ask for the hand-built list above, so be ready to write it too.
# python
# LC 146 - LRU Cache (library form)
# IDEA: OrderedDict keeps recency order: move_to_end on every use, popitem(last=False) evicts the oldest
# time = O(1) per get/put, space = O(capacity)
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict() # oldest key first, most recent last
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # a read is a use
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key) # an update is a use too
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # evict the least recently used
// java
// LC 146 - LRU Cache (library form)
// IDEA: LinkedHashMap with accessOrder = true reorders on every get/put; removeEldestEntry evicts
// time = O(1) per get/put, space = O(capacity)
class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // true = iterate in access order, not insertion order
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity; // called after every put
}
}
Complexity
- Time O(1) per call:
getandputdo one map lookup plus a constant number of pointer assignments inremove/add_to_tail; nothing walks the list. - Space O(capacity): the map and the list each hold at most capacity + 1 entries, and only for the moment between the insert and the eviction.
Dry run (capacity=2, LC 146 Example 1)
put(1,1): head <-> [1] <-> tail
put(2,2): head <-> [1] <-> [2] <-> tail
get(1): head <-> [2] <-> [1] <-> tail ← 1 moved to MRU, returns 1
put(3,3): head <-> [2] <-> [1] <-> [3] <-> tail ← size 3 > 2
evict head.next=[2]
head <-> [1] <-> [3] <-> tail
get(2): -1
put(4,4): evict head.next=[1]
head <-> [3] <-> [4] <-> tail
get(1) = -1, get(3) = 3, get(4) = 4
Traps
- The node must store its
key. Eviction reaches the victim throughhead.nextand must then delete its map entry; withoutnode.keythe map keeps a stale entry,len(kv_map)never shrinks, andgetreturns a node that is no longer in the list. - Evicting
headinstead ofhead.nextremoves the sentinel: the nextremovedereferencesNone. - Evicting before the existing-key check drops an entry on a plain update: capacity 2,
put(1,1) put(2,2) put(1,10)loses key 2. - Mixing orientations — adding at the tail but evicting
tail.prev— evicts the key just touched.
| Insert, then evict (template above) | Evict, then insert | |
|---|---|---|
| Capacity test | len(kv_map) > capacity after inserting |
len(kv_map) == capacity before inserting |
| Victim | head.next — the new node is at the tail, so never itself |
head.next |
| The trap | none extra | the eviction must sit inside the new-key branch; evicting before the existing-key check drops an entry on a plain update |
Not this if — eviction is by frequency, recency only breaking ties (LC 460) → one list per
frequency, below. A plain dict plus a list of keys looks like an LRU and is not one:
keys.remove(key) is O(n) per call.
| # | Problem | Key Difference |
|---|---|---|
| 146 | LRU Cache | Classic pattern — evict least recently used |
| 460 | LFU Cache | Two-level structure: one LRU list per frequency + min_freq — worked below |
| 432 | All O(1) Data Structure | Doubly linked list of count buckets |
| 1472 | Design Browser History | Doubly linked list, truncate forward on visit |
| 641 | Design Circular Deque | Doubly linked list with fixed capacity, both ends |
| 716 | Max Stack | Stack + doubly linked list + TreeMap for O(log n) popMax |
Drill — LC 146 ⭐ · LC 1472 (one cursor, truncate forward) · LC 641 (fixed capacity, both ends) · LC 460 (one list per frequency: below) · LC 432
LFU Variant — one list per frequency, plus a min_freq pointer (LC 460) Priority 4 of 5 — High value — a gap here costs you rounds
Key point — every per-frequency list is the LRU list above;
min_freqnames the list the victim lives in, and it only ever moves by+1on a touch or resets to1on an insert.
Reduces to: Template 7 — the same sentinel list, once per frequency.
What changed: LRU evicts by recency, so one list is enough — its head.next is always the
victim. LFU evicts by frequency, and recency only breaks ties — so the one list becomes one
LRU list per frequency, and an integer min_freq names the list the victim lives in. Each
per-frequency list is exactly the sentinel list from the template above; none of the pointer
surgery is new, only the bookkeeping around it.
Layout:
key_to_node : key -> Node(key, val, freq)
freq_to_list : freq -> head(dummy) <-> [LRU] <-> ... <-> [MRU] <-> tail(dummy)
min_freq : the smallest freq whose list is non-empty
freq 1: head <-> [c] <-> tail <- min_freq = 1, victim = this list's head.next
freq 2: head <-> [a] <-> [b] <-> tail <- a is older than b
freq 5: head <-> [d] <-> tail
The three moves — every operation is one or two of these, in this order:
| Move | What it does | Where min_freq goes |
|---|---|---|
touch(node) — get, and put on an existing key |
unlink from freq_to_list[f], f += 1, append to the tail of freq_to_list[f] |
if the list just left is now empty and f == min_freq: min_freq += 1. Exact, not a search — the node itself just landed in f + 1, so that list is non-empty |
evict — put on a new key, cache full |
the victim is freq_to_list[min_freq].head.next; unlink it and delete its map entry |
untouched — the insert that follows resets it |
insert — put on a new key |
a new Node(key, value, 1) appended to freq_to_list[1] |
min_freq = 1, every time — a brand-new key is the least frequent by definition |
Template Pattern:
# python
# LC 460 - LFU Cache
# IDEA: the LRU template, once per frequency. key_to_node finds the node in O(1);
# freq_to_list[f] is a sentinel doubly linked list of every key seen f times,
# LRU at head.next and MRU before tail; min_freq names the list the victim is in.
# time = O(1) per get/put, space = O(capacity)
class Node:
def __init__(self, key=0, val=0, freq=0):
self.key = key # for eviction to delete the map entry (as in LRU)
self.val = val
self.freq = freq # and freq, to know which list to unlink from
self.prev = None
self.next = None
class DLinkedList:
"""the LRU template's list: sentinel head/tail, O(1) unlink and append"""
def __init__(self):
self.head = Node() # LRU side
self.tail = Node() # MRU side
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
self.size -= 1
def add_to_tail(self, node): # insert just before tail (MRU)
prev_node = self.tail.prev
prev_node.next = node
node.prev = prev_node
node.next = self.tail
self.tail.prev = node
self.size += 1
def pop_head(self): # the LRU node of this frequency
node = self.head.next
self.remove(node)
return node
class LFUCache:
def __init__(self, capacity):
self.capacity = capacity
self.key_to_node = {} # key -> Node
self.freq_to_list = {} # freq -> DLinkedList
self.min_freq = 0
def touch(self, node):
"""move node from its freq list to the freq+1 list, keeping min_freq exact"""
old_list = self.freq_to_list[node.freq]
old_list.remove(node)
if old_list.size == 0:
del self.freq_to_list[node.freq] # keep the map tidy
# the minimum list just emptied, and this node is about to land one up
if self.min_freq == node.freq:
self.min_freq += 1
node.freq += 1
if node.freq not in self.freq_to_list:
self.freq_to_list[node.freq] = DLinkedList()
self.freq_to_list[node.freq].add_to_tail(node)
def get(self, key):
if key not in self.key_to_node:
return -1
node = self.key_to_node[key]
self.touch(node)
return node.val
def put(self, key, value):
if self.capacity == 0:
return
# case 1) key exists: update in place, count the access — never evicts
if key in self.key_to_node:
node = self.key_to_node[key]
node.val = value
self.touch(node)
return
# case 2) new key and the cache is full: evict the LRU node of the least-frequent list
if len(self.key_to_node) == self.capacity:
victim_list = self.freq_to_list[self.min_freq]
victim = victim_list.pop_head()
del self.key_to_node[victim.key]
if victim_list.size == 0:
del self.freq_to_list[self.min_freq]
# case 3) insert at freq 1 — a brand-new key is the least frequent by definition
node = Node(key, value, 1)
self.key_to_node[key] = node
if 1 not in self.freq_to_list:
self.freq_to_list[1] = DLinkedList()
self.freq_to_list[1].add_to_tail(node)
self.min_freq = 1
Visual Trace (capacity=2, LC 460 Example 1; each list shown LRU → MRU):
put(1,1): f1: [1] min_freq=1
put(2,2): f1: [1, 2] min_freq=1
get(1): f1: [2] f2: [1] -> 1 f1 still non-empty, min_freq stays 1
put(3,3): full -> evict f1.head.next = [2]
f1: [3] f2: [1] min_freq=1 (reset by the insert)
get(2): -1
get(3): f1: [] f2: [1, 3] -> 3 f1 emptied AND was the min -> min_freq=2
put(4,4): full -> evict f2.head.next = [1] 1 and 3 both have freq 2; 1 is the older
f1: [4] f2: [3] min_freq=1
get(1) = -1, get(3) = 3, get(4) = 4
Pitfalls — the ones that cost the O(1):
min_freq += 1is exact, never a scan. The only way the minimum list empties mid-run is its last node moving tomin_freq + 1, so the new minimum is known without looking. Amin(freq_to_list)here makes every touch O(#distinct frequencies).min_freq = 1on every new insert, not only when the cache was empty: the new key is the least frequent no matter what was there.- Evict before insert, and only on the new-key path.
puton an existing key is a touch, never an eviction — the same trap as LRU’s “evict, then insert” order. - The
capacity == 0guard is load-bearing. Without it the firstputevicts fromfreq_to_list[0], which does not exist. - The tie-break is recency inside one list. Append at the tail, evict at the head; mixing ends evicts the key just touched, exactly as in LRU.
- The node stores
freqas well askey.keyis for eviction to reach the map;freqis for a touch to reach the list it must leave.
Library shortcut: one OrderedDict per frequency — popitem(last=False) is pop_head,
appending a key is add_to_tail — and in Java one LinkedHashSet<Integer> per frequency. Same
structure, same min_freq rule, a third of the code:
# python
# LC 460 - LFU Cache (library form)
# IDEA: freq -> OrderedDict of keys (oldest first); min_freq names the bucket the victim lives in
# time = O(1) per get/put, space = O(capacity)
from collections import OrderedDict, defaultdict
class LFUCache:
def __init__(self, capacity):
self.capacity = capacity
self.key_to_val_freq = {} # key -> [value, freq]
self.freq_to_keys = defaultdict(OrderedDict) # freq -> keys, least recent first
self.min_freq = 0
def touch(self, key):
value, freq = self.key_to_val_freq[key]
del self.freq_to_keys[freq][key]
if not self.freq_to_keys[freq]:
del self.freq_to_keys[freq]
if self.min_freq == freq: # the minimum bucket just emptied
self.min_freq += 1
self.key_to_val_freq[key][1] = freq + 1
self.freq_to_keys[freq + 1][key] = None # appended = most recent in its bucket
def get(self, key):
if key not in self.key_to_val_freq:
return -1
self.touch(key)
return self.key_to_val_freq[key][0]
def put(self, key, value):
if self.capacity == 0:
return
if key in self.key_to_val_freq: # update: a touch, never an eviction
self.key_to_val_freq[key][0] = value
self.touch(key)
return
if len(self.key_to_val_freq) == self.capacity:
victim, _ = self.freq_to_keys[self.min_freq].popitem(last=False)
if not self.freq_to_keys[self.min_freq]:
del self.freq_to_keys[self.min_freq]
del self.key_to_val_freq[victim]
self.key_to_val_freq[key] = [value, 1]
self.freq_to_keys[1][key] = None
self.min_freq = 1
The structure pairing that gets you there is the table in design.md. LC 432 goes one step
further and threads the buckets themselves on a doubly linked list, so there is no
min_freq integer to maintain at all.
2) Pattern Selection
Linked-list problems are rarely about lists. They are about which handle you have to be holding when the surgery happens — and every technique on this sheet exists to make sure you are holding it. Pick by what the answer needs, not by the problem’s title.
| If the problem asks you to… | Reach for | Because | Written out at |
|---|---|---|---|
| delete or insert anywhere, head included | dummy head | it gives prev a standing position one node before the head, so “remove the head” stops being a special case |
Template 2 |
| remove every node matching a value | dummy + look-ahead on curr.next |
you have to be able to stay put after a deletion, or a run like [6,6,6] loses one |
Template 2 |
| split the nodes into two groups, each keeping its order — by value or by index | two dummies, one tail per chain, then join | p1.next = dummy_2.next joins without ever tracking chain 2’s head; p2.next = None stops the cycle |
Template 3 |
| reverse the whole list | the 3-step loop: cache next → flip → advance | O(1) space; the recursive form costs a frame per node for the same answer | Template 1 |
reverse a segment — [left, right], every k, or pairs |
the reverse-k helper, returning 3 handles | LC 92 / 25 / 24 differ only in where the segment is, never in how it reverses | Template 4 |
| find the middle, detect a cycle, or reach the n-th from the end | fast/slow pointers | one pass, O(1) space, and no length to precompute | Template 5, 2_pointers_linkedlist.md |
| reorder — interleave, split, rotate, palindrome-check | split with fast/slow → reverse the back half → merge | every reorder problem is those three primitives in sequence; none of them is new | LC 143, LC 234 |
| merge two sorted lists | dummy + a merge walk, splicing nodes rather than copying values | the tail pointer is the whole trick: cur.next = l1 or l2 finishes it |
LC 21 |
| merge k sorted lists, or sort one list | divide and conquer — pairwise merge, or merge sort via the middle | O(n log k) / O(n log n); a heap trades the recursion for O(k) space | heap § Template 3, LC 148 |
| do arbitrary-position reads and O(1) eviction — by recency, or by frequency | doubly linked list + hash map — one list, or one per frequency | the map gives you the node, the doubly-linked node gives you its neighbours — neither alone is enough | Template 7, LFU Variant, design.md |
| do arithmetic on digits stored as a list | carry loop over a dummy, reversing first if the list is most-significant-first | the carry outlives both inputs, so the loop condition is l1 or l2 or carry |
Template 6, LC 369 |
| answer a question that needs random access or a window | dump to an array first, then use the array technique | prefix sums and monotonic stacks need indices; a list has none, and O(n) extra space is usually allowed | LC 1171, LC 1019 |
The four traps
- Losing the list.
curr.next = prevbefore cachingcurr.nextthrows away everything downstream. Cache first — that is why the reversal loop is written in that order. - Returning the wrong head. After any operation that can touch the first node, return
dummy.next, nothead:headmay no longer be in the list. - Leaving a cycle behind. In the recursive reversal,
head.next.next = headwithout the followinghead.next = nullleaves the last two nodes pointing at each other. - Advancing past the end.
while (fast != null && fast.next != null)for a two-step hop. Getting the two clauses in the wrong order dereferences null on an even-length list.
3) Worked Examples
The problems that apply these templates are problem cards in linked_list_examples.md, one section per template, in the same order:
| Template | Problems |
|---|---|
| Iterative reversal | LC 143, 1019 |
| Dummy head deletion | LC 19, 147, 1171 |
| Two dummies, split then join | LC 21, 725, 817 |
| Reverse a segment | LC 24, 430, 25 |
| Fast/slow middle | pointers to LC 160, 234, 142, 148 |
| Carry loop | LC 369 |
| Hash map + doubly linked list | LC 138 |
4) Drill Path
| Stage | Problems (easy → hard) | What it adds |
|---|---|---|
| Core ⭐ | LC 206 again · LC 21 ok · LC 141 ok · LC 19 again · LC 143 again · LC 146 again · LC 23 again | reversal, one dummy and a merge walk, fast/slow (cycle and gap), the three reorder primitives together, the cache design, then k-way merging |
| Practice | LC 203 no verdict · LC 876 no verdict · LC 83 again · LC 234 ok · LC 2 no verdict · LC 86 again · LC 92 again · LC 24 again · LC 328 no verdict · LC 82 no verdict · LC 445 no verdict · LC 148 ok | each template once on its anchor problem, then a variation of it |
| Stretch | LC 138 again · LC 25 no verdict · LC 460 no verdict · LC 432 no verdict | copy with a node map; reverse per group; one list per frequency; buckets on a list |