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
-
Use “pseudo head node” 虛擬頭節點
-
When delete node from linked list, we need to be at “previous” node, then can delete NEXT node
- so, need to be at
curnode, than cancur.nextnode
python# python # https://youtu.be/Y4oQJklHxVo?t=965 cur.next = cur.next.next - so, need to be at
# 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
0-2) Pattern
Dummy Head Technique
Definition: Create a dummy/pseudo head node that points to the actual head, making it easier to handle edge cases and node removal operations.
When to Use:
- Removing nodes from the beginning of the list
- When the head node might be modified
- Simplifying edge case handling
- Operations that need to track the previous node
Time Complexity: O(n) - same as without dummy head Space Complexity: O(1) - only one extra node
Template Pattern:
def linked_list_operation(head):
# Create dummy head
dummy = ListNode(0)
dummy.next = head
# Use prev to track previous node
prev = dummy
curr = head
while curr:
# Perform operations
if condition:
# Remove current node
prev.next = curr.next
else:
prev = curr
curr = curr.next
# Return new head (dummy.next)
return dummy.next
Advantages:
- Eliminates need for special handling of head node
- Simplifies code logic
- Reduces edge case bugs
- Consistent prev pointer throughout traversal
Why Dummy Node? Visual Comparison (LC 19)
Problem: Remove the n-th node from the end of
[1, 2, 3, 4, 5].
Case A — Normal removal: n = 2 (remove node 4)
Without dummy — works fine here:
fast = slow = head = [1]
Step 1: move fast n=2 steps ahead
[1] -> [2] -> [3] -> [4] -> [5]
^slow ^fast
Step 2: move both until fast.next is None
[1] -> [2] -> [3] -> [4] -> [5]
^slow ^fast
Step 3: slow.next = slow.next.next → removes [4]
[1] -> [2] -> [3] -> [5] ✓
With dummy — also works, same logic:
fast = slow = dummy[0]
Step 1: move fast n+1=3 steps ahead
[0] -> [1] -> [2] -> [3] -> [4] -> [5]
^slow ^fast
Step 2: move both until fast is None
[0] -> [1] -> [2] -> [3] -> [4] -> [5]
^slow ^fast → None (stop)
Step 3: slow.next = slow.next.next → removes [4]
[0] -> [1] -> [2] -> [3] -> [5] → return dummy.next = [1] ✓
Case B — Edge case: n = 5 (remove the head node 1)
Without dummy — BREAKS, needs special-case code:
fast = slow = head = [1]
Step 1: move fast n=5 steps
fast: 1 -> 2 -> 3 -> 4 -> 5 -> None
[1] -> [2] -> [3] -> [4] -> [5] -> None
^slow ^fast (None!)
Step 2: while fast.next → fast is None, loop NEVER runs
slow is still at [1] (the head itself!)
Step 3: slow.next = slow.next.next
→ This removes [2], NOT the head — WRONG ❌
Must add a special case:
if not fast:
return head.next # ← extra branch needed
With dummy — works uniformly, NO special case:
fast = slow = dummy[0]
Step 1: move fast n+1=6 steps ahead
fast: dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None
[0] -> [1] -> [2] -> [3] -> [4] -> [5] -> None
^slow ^fast (None)
Step 2: while fast → fast is None, loop NEVER runs
slow stays at dummy[0] ← one node BEFORE the head
Step 3: slow.next = slow.next.next
dummy.next = [2] → head [1] is removed ✓
Return dummy.next = [2] -> [3] -> [4] -> [5] ✓ No special case!
Summary: Why dummy wins
| Without Dummy | With Dummy | |
|---|---|---|
| Normal removal | ✓ Works | ✓ Works |
| Remove head (n = len) | ❌ Needs if not fast: return head.next |
✓ Works uniformly |
| Code branches | Extra conditional | None |
slow start position |
head (can’t reach before head) |
dummy (one step before head) |
Key insight: the dummy node gives slow a “standing position” one node before the head, so it can reconnect across any node — including the head itself — without special handling.
# LC 19 — with dummy (handles all cases cleanly)
def removeNthFromEnd(self, head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
for _ in range(n + 1): # fast moves n+1 steps
fast = fast.next
while fast: # move both until fast is None
fast = fast.next
slow = slow.next
slow.next = slow.next.next # remove the target node
return dummy.next
Dummy Head — Other Applications
Two more problems where the dummy is what removes the special case. The rest of the family is worked where it belongs, not re-pasted here:
| LC | Problem | Worked in |
|---|---|---|
| 19 | Remove Nth Node From End | the visual walkthrough above, and linked_list_examples.md for both Java forms |
| 21 | Merge Two Sorted Lists | linked_list_examples.md |
| 2 | Add Two Numbers | 1-1-7) below |
| 203 | Remove Linked List Elements | Remove Elements by Value Pattern below |
Remove duplicates from a sorted list — LC 83: the dummy holds the last kept node, so a run of equal values collapses without ever special-casing a duplicated head.
def deleteDuplicates(self, head):
dummy = ListNode(0)
dummy.next = head
prev = dummy
while head and head.next:
if head.val == head.next.val:
# Skip all duplicates
val = head.val
while head and head.val == val:
head = head.next
prev.next = head
else:
prev = head
head = head.next
return dummy.next
Partition List — LC 86: two dummies. Build the < x chain and the >= x chain
independently, then join them — no in-place surgery, and stability falls out for free.
def partition(self, head, x):
before_dummy = ListNode(0)
after_dummy = ListNode(0)
before = before_dummy
after = after_dummy
while head:
if head.val < x:
before.next = head
before = before.next
else:
after.next = head
after = after.next
head = head.next
# Connect the two parts
after.next = None
before.next = after_dummy.next
return before_dummy.next
Key Benefits of Dummy Head:
| Aspect | Without Dummy | With Dummy |
|---|---|---|
| Edge Cases | Complex head handling | Unified approach |
| Code Length | More conditional logic | Cleaner, shorter |
| Bug Probability | Higher (edge cases) | Lower (consistent) |
| Readability | Harder to follow | More intuitive |
Related Problems:
- LC 19: Remove Nth Node From End of List
- LC 21: Merge Two Sorted Lists
- LC 83: Remove Duplicates from Sorted List
- LC 86: Partition List
- LC 203: Remove Linked List Elements
- LC 328: Odd Even Linked List
Remove Elements by Value Pattern
Definition: Remove all nodes from a linked list that match a specific value. Uses dummy head and a “look ahead” technique where the current pointer examines curr.next rather than curr itself.
Core Concept:
- Key Insight: When we find a node to remove, we ONLY update the pointer connection (
curr.next = curr.next.next), but thecurrpointer itself does NOT move forward - This allows handling consecutive matching nodes (e.g.,
[6,6,6,3]with val=6) - Only move
currforward whencurr.next.val != val
When to Use:
- Removing nodes by value from anywhere in the list
- Handling cases where head node(s) might need removal
- Removing consecutive duplicate values
Time Complexity: O(n) Space Complexity: O(1)
Template Pattern:
// Java
public ListNode removeElements(ListNode head, int val) {
// 1. Create dummy node pointing to head
ListNode dummy = new ListNode(0);
dummy.next = head;
// 2. Use curr pointer (starts at dummy, looks ahead)
ListNode curr = dummy;
// 3. Look ahead at NEXT node
while (curr.next != null) {
if (curr.next.val == val) {
// Found match - skip the next node
// NOTE: curr does NOT move!
curr.next = curr.next.next;
} else {
// No match - move pointer forward
curr = curr.next;
}
}
// 4. Return actual head
return dummy.next;
}
# Python
def removeElements(self, head: ListNode, val: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
curr = dummy
while curr.next:
if curr.next.val == val:
curr.next = curr.next.next # skip, don't move curr
else:
curr = curr.next # move forward
return dummy.next
Dry Run Example ([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]
Why This Works for Consecutive Matches:
| Scenario | Without “stay in place” | With “stay in place” |
|---|---|---|
[6,6,3] val=6 |
Would skip second 6 | Catches all 6s |
| Head removal | Needs special case | Handled uniformly |
Similar LC Problems:
- LC 203: Remove Linked List Elements (exact pattern)
- LC 83: Remove Duplicates from Sorted List (similar, compare adjacent)
- LC 82: Remove Duplicates from Sorted List II (remove all duplicates)
- LC 237: Delete Node in a Linked List (different - no access to prev)
- LC 1474: Delete N Nodes After M Nodes (pattern variation)
- LC 2487: Remove Nodes From Linked List (stack-based variation)
Doubly Linked List + HashMap (LRU Cache Pattern) Priority 5 of 5 — Must know — expect it in almost every loop
Core Idea: Combine a HashMap for O(1) key lookup with a doubly linked list for O(1) ordered eviction. Most-recently-used nodes sit near the tail; least-recently-used sits near the head. Sentinel dummy head/tail nodes eliminate all edge-case pointer checks.
Layout:
head(dummy) <-> [LRU] <-> ... <-> [MRU] <-> tail(dummy)
When to Use:
- Need O(1) get + O(1) put with ordered eviction (LRU/MFU)
- Any problem requiring an ordered, access-tracked collection
Time Complexity: O(1) get and put
Space Complexity: O(capacity)
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
Template Pattern:
# 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 # NOTE !!! 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]
Visual Trace (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
Why sentinel nodes?
removeandadd_to_tailalways have valid.prev/.nextneighbors- No
if node.prev is Noneorif node.next is Noneguards needed - Works uniformly for head removal, tail removal, and middle removal
- The LRU is
head.next, neverhead—headis a dummy with no map entry
Why does the node store its key? The map goes key → node, but eviction arrives from
the other direction: it reaches the victim through head.next, and must then delete that
victim’s map entry. Without node.key there is no O(1) way back from the node to the map —
forget it and the map keeps a stale entry, so len(kv_map) never shrinks and get returns a
node that is no longer in the list.
Two orders for put, both correct:
| 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 |
Orientation is a convention, not the pattern. Some solutions insert MRU right after head
(add_to_head) and evict tail.prev. It is the same structure mirrored: insert at one end,
evict from the other. Mixing the two — adding at the tail but evicting tail.prev — evicts the
key just touched.
Library shortcut: Python’s OrderedDict (move_to_end(key), popitem(last=False)) and
Java’s LinkedHashMap (access-order constructor + removeEldestEntry) are this hash map +
doubly linked list. Say so in an interview, then be ready to build it by hand — the OrderedDict
form is worked in design_examples.md 1).
Similar LC Problems:
| # | 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 |
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
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 # NOTE !!! for eviction to delete the map entry (as in LRU)
self.val = val
self.freq = freq # NOTE !!! 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,
move_to_end is the append — and in Java one LinkedHashSet<Integer> per frequency. That form
is worked in design_examples.md 2); 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.
Reverse K Nodes Helper Pattern Priority 5 of 5 — Must know — expect it in almost every loop
Core Idea: Almost every “reverse a segment” problem (LC 92, LC 25, LC 24, LC 206) is the same primitive — reverse k nodes starting from a head, then reconnect. Factor that primitive into a single reusable helper so the outer solution only worries about locating the segment and stitching the ends back together.
The helper reverses k nodes and returns three handles you need to reconnect cleanly:
# python — reusable helper: reverse k nodes starting at `head`
# time = O(k), space = O(1)
def reverse_helper(self, head, k):
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
# prev = new head of reversed list (was the k-th node)
# head = new tail (original head, now points forward to `curr`)
# curr = first node AFTER the reversed segment
return prev, head, curr
Why return 3 things? After reversing an inner segment you must re-wire both boundaries:
| 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 |
When to Use:
- Reverse a sub-range
[left, right](LC 92) → reverseright - left + 1nodes - Reverse every k-group (LC 25) → call helper in a loop until fewer than
kremain - Reverse whole list (LC 206) → call helper once with
k = length(ork = ∞)
Template — apply helper to LC 92 (Reverse Linked List II):
# python
# LC 92 - reverse nodes from position `left` to `right`
# time = O(n), space = O(1)
class Solution(object):
def reverseBetween(self, head, left, right):
# edge case
if not head or left == right:
return head
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) `start` = first node of the segment to reverse
start = prev.next
# 3) reverse (right - left + 1) nodes via the helper
new_head, new_tail, next_node = self.reverse_helper(
start, right - left + 1
)
# 4) 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):
prev = None
curr = head
while curr and k > 0:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
k -= 1
return prev, head, curr
Visualization ([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] ✓
The 3 boundary handles, visually:
prev new_head → ... → new_tail next_node
| | | |
... -> 1 4 -> 3 -> 2 (dangling) 5 -> ...
|_____________| |_______________|
(C1) prev.next = new_head (C2) new_tail.next = next_node
Reusing the helper for LC 25 (Reverse Nodes in k-Group):
# python
# LC 25 - reverse every k nodes; leave the tail (< k) as-is
# time = O(n), space = O(1)
class Solution(object):
def reverseKGroup(self, head, k):
# count if >= k nodes remain
def has_k(node, k):
cnt = 0
while node and cnt < k:
node = node.next
cnt += 1
return cnt == k
dummy = ListNode(0)
dummy.next = head
prev = dummy # node before current group
while has_k(prev.next, k):
start = prev.next
new_head, new_tail, next_node = self.reverse_helper(start, k)
prev.next = new_head # front of group
new_tail.next = next_node # tail of group -> rest
prev = new_tail # move `prev` to end of this group
return dummy.next
def reverse_helper(self, head, k):
prev = None
curr = head
while curr and k > 0:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
k -= 1
return prev, head, curr
Key insight: the same
reverse_helperpowers LC 206 / 92 / 25. Only the surrounding logic differs — 206 calls it once, 92 locates one segment then calls it once, 25 loops and calls it per group. Master the 3-handle return (new_head, new_tail, next_node) and all three collapse into “locate → reverse → reconnect”.
Similar LC Problems:
| # | Problem | How the helper applies |
|---|---|---|
| 206 | Reverse Linked List | One call, k = length — only new_head matters |
| 92 | Reverse Linked List II | Locate segment, one call with k = right - left + 1, reconnect both ends |
| 25 | Reverse Nodes in k-Group | Loop the helper per group; skip the final < k tail |
| 24 | Swap Nodes in Pairs | Special case k = 2 — so small the helper is not worth calling; worked below |
| 61 | Rotate List | Different op, but same “locate boundary + re-stitch” discipline |
Pairwise Swap — the k = 2 instance (LC 24)
At k = 2 the helper still works — reverse_helper(start, 2) plus the LC 25 loop solves LC 24
unchanged — but reversing two nodes is two assignments, so the loop inside the helper earns
nothing. Unroll it and the three handles stop being return values and become local names:
| The helper returns | At k = 2 it is |
Which is written as |
|---|---|---|
new_head |
second |
prev.next = second |
new_tail |
first |
prev = first — the next iteration’s prev |
next_node |
second.next |
folded straight into first.next = second.next |
# python
# LC 24 - Swap Nodes in Pairs
# IDEA: the k = 2 case of the helper above, unrolled — only the reconnection survives.
# `prev` is the ONLY cursor: both members of the pair are reachable from it,
# so there is no second `head` walker to keep in step.
# time = O(n), space = O(1)
class Solution(object):
def swapPairs(self, head):
dummy = ListNode(0)
dummy.next = head
prev = dummy # node BEFORE the pair
# prev.next and prev.next.next ARE the pair -> no separate cursor needed
while prev.next and prev.next.next:
first = prev.next # becomes the pair's tail (new_tail)
second = first.next # becomes the pair's front (new_head)
# reverse the 2-node segment
first.next = second.next # (A) carry `next_node` straight into the new tail
second.next = first # (B) flip the pair
# reconnect the front
prev.next = second # (C) prev adopts the new front
# prev = new_tail: after the swap `first` sits before the next pair
prev = first
return dummy.next
Why prev alone is enough. The loop condition reads the pair through prev
(prev.next, prev.next.next), so the cursor that guards the loop and the cursor that owns
the incoming link are the same node. A form that also walks a separate head pointer has to
advance both, and the two can fall out of step. It is also why the empty and single-node cases
need no guard: prev.next.next is already None, so the loop simply never runs.
input: 1 -> 2 -> 3 -> 4
dummy -> 1 -> 2 -> 3 -> 4
^prev ^first
^second
(A) first.next = second.next # 1 -> 3
(B) second.next = first # 2 -> 1
(C) prev.next = second # dummy -> 2
dummy -> 2 -> 1 -> 3 -> 4
prev = first
dummy -> 2 -> 1 -> 3 -> 4
^prev
^first
^second <- next iteration reads the pair off prev again
... repeat on (3, 4); then prev.next.next is None -> stop
return dummy.next => 2 -> 1 -> 4 -> 3
Key insight:
prev = firsthere andprev = new_tailin the LC 25 loop are the same move — a segment’s old head is its new tail, which is exactly the node that must own the incoming link for the next segment. Recognise that once and LC 24 stops being its own problem.
The full walkthrough — the three anchors, why (A) must precede (B), the per-iteration dry
run and the recursive form — is in
linked_list_examples.md 3).
1) General form
// java
// single Linklist
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
# python
class Node:
"""
# constructor
# A single node of a singly linked list
"""
def __init__(self, data=None, next=None):
self.data = data
self.next = next
class LinkedList:
"""
# A Linked List class with a single head node
"""
def __init__(self):
self.head = Node()
def get_length(self):
"""
# get list length method for the linked list
i.e.
before : 1 -> 2 -> 3
after : 3
"""
current = self.head
length = 0
while current:
current = current.next
length += 1
return length
def get_tail(self):
"""
# get list tail method for the linked list
i.e.
before : a -> b -> c
after : c
"""
current = self.head
while current:
current = current.next
return current
def print(self):
"""
# print method for the linked list
i.e.
before : 1 -> 2 -> 3
after : 1 2 3
"""
current = self.head
while current:
print (current.data)
current = current.next
def append(self, data):
"""
# append method that append a new item at the end of the linkedlist
i.e.
before : 1 -> 2 -> 3
after : 1 -> 2 -> 3 -> 4
"""
newNode = Node(data)
if self.head:
current = self.head
while current.next:
current = current.next
current.next = newNode
else:
self.head = newNode
def prepend(self, data):
"""
# append method that append a new item at the head of the linkedlist
i.e.
before : 1 -> 2 -> 3
after : 0 -> 1 -> 2 -> 3
"""
newNode = Node(data)
if self.head:
current = self.head
self.head = newNode
newNode.next = current
current = current.next
else:
self.head = newNode
def insert(self, idx, data):
"""
# append method that append a new item within the linkedlist
i.e.
before : 1 -> 2 -> 3
insert(1, 2.5)
after : 1 -> 2 -> 2.5 -> 3
before : 1 -> 2 -> 3
insert(0, 0)
after : 0 -> 1 -> 2 -> 3
before : 1 -> 2 -> 3
insert(2, 4)
after : 1 -> 2 -> 3 -> 4
"""
current = self.head
ll_length = self.get_length()
if idx < 0 or idx > self.get_length():
print ("idx out of linkedlist range, idx : {}".format(idx))
return
elif idx == 0:
self.prepend(data)
elif idx == ll_length:
self.append(data)
else:
newNode = Node(data)
current = self.head
cur_idx = 0
while cur_idx < idx-1:
current = current.next
cur_idx += 1
newNode.next = current.next
current.next = newNode
def remove(self, idx):
"""
# remove method for the linked list
i.e.
before : 1 -> 2 -> 3
remove(1)
after : 1 -> 3
before : 1 -> 2 -> 3
remove(2)
after : 1 -> 2
before : 1 -> 2 -> 3
remove(0)
after : 2 -> 3
"""
if idx < 0 or idx > self.get_length():
print ("idx out of linkedlist range, idx : {}".format(idx))
return
elif idx == 0:
current = self.head
self.head = current.next
elif idx == self.get_length():
current = self.head
cur_idx = 0
while cur_idx < idx -1:
current = current.next
cur_idx += 1
current.next = None
else:
current = self.head
cur_idx = 0
while cur_idx < idx - 1:
current = current.next
cur_idx += 1
next_ = current.next.next
current.next = next_
current = next_
def reverse(self):
"""
https://www.youtube.com/watch?v=D7y_hoT_YZI
# reverse method for the linked list
# https://www.geeksforgeeks.org/python-program-for-reverse-a-linked-list/
i.e.
before : 1 -> 2 -> 3
after : 3 -> 2 -> 1
"""
prev = None
current = self.head
while(current is not None):
next_ = current.next
current.next = prev
prev = current
current = next_
self.head = prev
1-1) Basic OP
1-1-1) Reverse linked list (iteration) — LC 206
# python
#-------------------------
# iteration
#-------------------------
# LC 206
# V0
# IDEA : Linkedlist basics
# https://www.youtube.com/watch?v=D7y_hoT_YZI
# STEPS)
# -> STEP 1) cache "next"
# -> STEP 2) point head.next to prev
# -> STEP 3) move prev to head
# -> STEP 4) move head to "next"
class Solution(object):
def reverseList(self, head):
# edge case
if not head:
return
prev = None
while head:
# cache "next"
tmp = head.next
# point head.next to prev
head.next = prev
# move prev to head (for next iteration)
prev = head
# move head to "next" (for next iteration)
head = tmp
# NOTE!!! we return prev
return prev
// java
//---------------------------
// iteration
//---------------------------
// LC 206
// V0
public ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode _prev = null;
while (head != null) {
/**
* NOTE !!!!
*
* 4 operations
*
* step 1) cache next
* step 2) point cur to prev
* step 3) move prev to cur
* step 4) move cur to next
*
*/
ListNode _next = head.next;
head.next = _prev;
_prev = head;
head = _next;
}
// NOTE!!! we return _prev here, since it's now "new head"
return _prev;
}
1-1-2) Reverse linked list (recursion) — LC 206
// java
//---------------------------
// recursion
//---------------------------
// LC 206
// algorithm book (labu) p.290
// 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)
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);
/** NOTE !!!
*
* head.next is the node that now sits BEHIND head in the reversed list,
* so pointing it back at head is the whole flip.
*/
head.next.next = head;
// cut the old forward link, or the two nodes form a 2-cycle
head.next = null;
return newHead;
}
1-1-3) Reverse nodes in [a,b] linked list (iteration) — LC 92
// java
//---------------------------
// iteration
//---------------------------
// algorithm book (labu) p.298
ListNode reverse(ListNode a, Listnode b){
ListNode pre, cur, nxt;
pre = null;
cur = a;
nxt = a;
/** THE ONLY DIFFERENCE (reverse nodes VS reverse nodes in [a,b]) */
while (cur != b){
nxt = cur.next;
// reverse on each node
cur.next = pre;
// update pointer
pre = cur;
cur = nxt;
}
// return reversed nodes
return pre;
}
1-1-4) Reverse nodes in k group linked list (iteration) — LC 25
// java
//---------------------------
// iteration
//---------------------------
// LC 25
// algorithm book (labu) p.298
/** NOTE !!! `reverse(a, b)` is exactly the primitive from 1-1-3) above — reverse the
* half-open interval [a, b) and return its new head. Reproduced there, not here. */
ListNode reverseKGroup(ListNode head, int k){
if (head == null) return null;
// inverval [a,b] has k to-reverse elements
ListNode a, b;
a = b = head;
for (int i = 0; i < k; i++){
// not enough elements (amount < k), no need to reverse -> base case
if (b == null) return head;
b = b.next;
}
// reverse k elements
ListNode newHead = reverse(a,b);
// reverse remaining nodes, and connect with head
a.next = reverseKGroup(b,k);
return newHead;
}
# LC 025
class Solution:
def reverseKGroup(self, head, k):
# help func
# check if # of sub nodes still > k
def check(head, k):
ans = 0
while head:
ans += 1
if ans >= k:
return True
head = head.next
return False
# edge case
if not head:
return
d = dummy = ListNode(None)
pre = None
preHead = curHead = head
while check(curHead, k):
for _ in range(k):
# reverse linked list
tmp = curHead.next
curHead.next = pre
pre = curHead
curHead = tmp
# reverse linked list
# ???
dummy.next = pre
dummy = preHead
preHead.next = curHead
preHead = curHead
return d.next
1-1-5) Reverse first N linked list (recursion)
//---------------------------
// recursion
//---------------------------
// java
// algorithm book (labu) p.293
// "postorder" node
ListNode successor = null;
// reverse first N node (from head), and return new head
ListNode reverseN(ListNode head, int n){
if (n == 1){
// record n + 1 nodes, will be used in following steps
successor = head.next;
return head;
}
// set head.next as start point, return first n - 1 nodes
ListNode last = reverseN(head.next, n-1);
head.next.next = head;
// connect reversed head node and following nodes
head.next = successor;
return last;
}
1-1-6) Reverse middle N nodes in linked list (start, end as interval) (recursion) — LC 92
// java
//---------------------------
// recursion
//---------------------------
// algorithm book (labu) p.293
/** NOTE !!! `reverseN` is the primitive from 1-1-5) above, unchanged. Only the
* `reverseBetween` wrapper below is new: walk m down to 1, then reverse the first n. */
// reverse nodes in index = m to index = n
ListNode reverseBetween(ListNode head, int m, int n){
// base case
if (m == 1){
return reverseN(head, n);
}
// for head.next, the op is reverse interval : [m-1, n-1]
// will trigger base case when when meet reverse start point
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
1-1-7) add 2 linked list — LC 2
# LC 002
class Solution(object):
def addTwoNumbers(self, l1, l2):
"""
NOTE :
1. we init linkedlist via ListNode()
2. we NEED make extra head refer same linkedlist, since we need to return beginning of linkedlist of this func, while res will meet "tail" at the end of while loop
"""
head = res = ListNode()
plus = 0
tmp = 0
while l1 or l2:
tmp += plus
plus = 0
if l1:
tmp += l1.val
l1 = l1.next
if l2:
tmp += l2.val
l2 = l2.next
if tmp > 9:
tmp -= 10
plus = 1
res.next = ListNode(tmp)
res = res.next
tmp = 0
### NOTE : need to deal with case : l1, l2 are completed, but still "remaining" plus
if plus != 0:
res.next = ListNode(plus)
res = res.next
#print ("res = " + str(res))
#print ("head = " + str(head))
return head.next
# LC 445 Add Two Numbers II
# V0
# IDEA : string + linked list
# DEMO
# input :
# [7,2,4,3]
# [5,6,4]
# intermedia output :
# l1_num = 7243
# l2_num = 564
class Solution:
def addTwoNumbers(self, l1, l2):
if not l1 and not l2:
return None
l1_num = 0
while l1:
l1_num = l1_num * 10 + l1.val
l1 = l1.next
l2_num = 0
while l2:
l2_num = l2_num * 10 + l2.val
l2 = l2.next
print ("l1_num = " + str(l1_num))
print ("l2_num = " + str(l2_num))
### NOTE : trick here :
# -> get int format of 2 linked list first (l1, l2)
# -> then sum them (l1_num + l2_num)
lsum = l1_num + l2_num
head = ListNode(None)
cur = head
### NOTE : go thrpigh the linked list int sum, append each digit to ListNode and return it
for istr in str(lsum):
cur.next = ListNode(int(istr))
cur = cur.next
# NOTE : need to return head (but not cur, since cur already meet the end of ListNode)
return head.next
1-1-8) Find linked list middle point — LC 876
// algorithm book p. 286
// java
Listnode slow, fast;
slow = fast = head;
while (fast && fast.next){
fast = fast.next.next;
slow = slow.next;
}
// slow pointer will be linked list middle point
// if element count in linked list is odd (TO VERIFY)
if (fast != null){
slow = slow.next;
}
# LC 876 Middle of the Linked List
# V0
# IDEA : fast, slow pointers + linkedlist
class Solution(object):
def middleNode(self, head):
# edge case
if not head:
return
s = f = head
while f and f.next:
# if not f:
# break
f = f.next.next
s = s.next
return s
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 |
Dummy Head Technique |
| 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 |
Remove Elements by Value Pattern |
| 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 | 1-1-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 | Reverse K Nodes Helper Pattern |
| 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 | 1-1-8), 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 | examples 2), 7) |
| 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 |
examples 4) |
| 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 | examples 5), 14), heap.md |
| 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 | Doubly Linked List + HashMap, 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 |
1-1-7), examples 13) |
| 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 | examples 15), 16) |
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 full solutions moved to linked_list_examples.md so the templates above are not buried under them. Seventeen problems, grouped by the technique each one exercises:
| Group | Problems |
|---|---|
| Reversal & reordering | LC 92, 143, 24 |
| Merging & splitting | LC 21, 23, 725 |
| Fast/slow pointers & structure | LC 234, 160, 19 |
| Copying, flattening & components | LC 138, 817, 430 |
| Arithmetic & sorting | LC 369, 148, 147 |
| Array techniques borrowed onto a list | LC 1171, 1019 |