Coding patternsArrays and strings
Linked lists
The linked list coding pattern for interviews. A dummy head, fast and slow pointers, reversing in place and merging, with a step-through, templates in Java, Python, C#, Go and TypeScript, and 10 LeetCode problems.
- Read first
- Two pointers
A linked list is a chain of nodes. Each node holds a value and a pointer to the next node, and nothing else. You can’t jump to the fifth node. You have to walk there from the head, the first node. That one limit shapes every linked list problem.
The good news is that the problems use only a few moves. Walk with one pointer. Walk with two pointers at different speeds. Flip a pointer to reverse the list. Splice nodes from two lists into one. The algorithms are short. The bugs come from losing a node: you change a pointer before you saved where it led, and the rest of the list is gone. So the real skill is knowing, at every line, which pointers you still need.
How to spot it
Section titled “How to spot it”- The input is a
ListNode. Then these moves are your toolbox, and you rarely need anything else. - “In place” or “O(1) extra memory” on a list. Copying the n values into an array is easy, but it uses O(n) memory and is usually not the answer wanted.
- The middle, the kth node from the end, or a loop. Two pointers with a gap or a speed difference find these in one pass.
- Reorder, rotate, swap pairs or reverse a part. These are cut, reverse and reconnect steps.
- Merge sorted lists. Two lists use the merge loop. With k lists, add a heap.
A list is also hiding inside other problems. An LRU cache uses a doubly linked list, where each node also points back to the one before it. That gives O(1) removal from the middle.
Watch it run
Section titled “Watch it run”Reverse the list 1 → 2 → 3 → 4 → 5. Watch the arrow between the reversed part and the rest. When
node 1’s arrow flips, nothing points from node 1 to node 2 anymore. Only next_node still knows
where node 2 is. Saving it first is the whole trick.
- previous, current = None, head
- while current:
- next_node = current.next # save the rest
- current.next = previous # flip one arrow
- previous, current = current, next_node
- return previous # the new head
Each node knows only the node after it. Reverse the list without a second list: flip the arrows one at a time.
At every step the list is cut into two clean chains.
previous is the head of the reversed part, and current is the head of the part still to do. Each
step moves one node from the second chain to the first. That’s the invariant to say out loud, and it
also tells you the answer at the end. When the second chain is empty, previous is the whole
reversed list.
The template
Section titled “The template”Reverse in place, merge two sorted lists with a dummy head, and find the middle with fast and slow pointers:
import java.util.ArrayList;import java.util.List;
public class LinkedLists { static ListNode reverseList(ListNode head) { // Reverse in place. Flipping current.next cuts us off from the rest of the list, so we save // it in next first. previous is always the head of the part already reversed. O(n) time, // O(1) memory. ListNode previous = null, current = head; while (current != null) { ListNode next = current.next; // save the rest before the link to it is lost current.next = previous; // flip one arrow previous = current; current = next; } return previous; // the old tail is the new head }
static ListNode mergeTwoLists(ListNode list1, ListNode list2) { // Merge two sorted lists. The dummy node gives the result a head before we know which list // wins the first place, so there's no special case for the first node. O(n + m) for lists // of n and m nodes. ListNode dummy = new ListNode(0), tail = dummy; while (list1 != null && list2 != null) { if (list1.val <= list2.val) { // <= keeps equal values in their original order tail.next = list1; list1 = list1.next; } else { tail.next = list2; list2 = list2.next; } tail = tail.next; } // What is left is already sorted, so link it whole. tail.next = list1 != null ? list1 : list2; return dummy.next; }
static ListNode middleNode(ListNode head) { // Fast moves two steps for each one slow moves, so when fast runs out, slow is halfway. // One pass and O(1) memory, without counting the length first. ListNode slow = head, fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; // for an even length, the second of the two middles }
public static void main(String[] args) { check(values(reverseList(build(1, 2, 3, 4, 5))).equals(List.of(5, 4, 3, 2, 1))); check(reverseList(null) == null); check(values(mergeTwoLists(build(1, 4, 6), build(2, 3, 7, 9))) .equals(List.of(1, 2, 3, 4, 6, 7, 9))); check(values(mergeTwoLists(null, build(5))).equals(List.of(5))); check(middleNode(build(1, 2, 3, 4, 5)).val == 3); check(middleNode(build(1, 2, 3, 4)).val == 3); System.out.println("ok: reverseList, mergeTwoLists and middleNode"); }
static ListNode build(int... numbers) { ListNode dummy = new ListNode(0), tail = dummy; for (int number : numbers) { tail.next = new ListNode(number); tail = tail.next; } return dummy.next; }
static List<Integer> values(ListNode node) { List<Integer> result = new ArrayList<>(); for (; node != null; node = node.next) result.add(node.val); return result; }
static void check(boolean ok) { if (!ok) throw new AssertionError(); }}
class ListNode { int val; ListNode next;
ListNode(int val) { this.val = val; }}class ListNode: def __init__(self, val: int = 0, next: "ListNode | None" = None): self.val = val self.next = next
# Reverse in place. Flipping current.next cuts us off from the rest of the list, so we save it in# next_node first. previous is always the head of the part already reversed. O(n) time for n nodes,# O(1) memory.def reverse_list(head: ListNode | None) -> ListNode | None: previous, current = None, head while current: next_node = current.next # save the rest before the link to it is lost current.next = previous # flip one arrow previous, current = current, next_node return previous # the old tail is the new head
# Merge two sorted lists. The dummy node gives the result a head before we know which list wins the# first place, so there's no special case for the first node. O(n + m) time for lists of n and m# nodes.def merge_two_lists(list1: ListNode | None, list2: ListNode | None) -> ListNode | None: dummy = tail = ListNode() while list1 and list2: if list1.val <= list2.val: # <= keeps equal values in their original order tail.next, list1 = list1, list1.next else: tail.next, list2 = list2, list2.next tail = tail.next tail.next = list1 or list2 # whatever is left is already sorted, so link it whole return dummy.next
# Fast moves two steps for each one slow moves, so when fast runs out, slow is halfway.# One pass and O(1) memory, without counting the length first.def middle_node(head: ListNode | None) -> ListNode | None: slow = fast = head while fast and fast.next: slow, fast = slow.next, fast.next.next return slow # for an even length, the second of the two middles
def build(numbers: list[int]) -> ListNode | None: dummy = tail = ListNode() for number in numbers: tail.next = ListNode(number) tail = tail.next return dummy.next
def values(node: ListNode | None) -> list[int]: result = [] while node: result.append(node.val) node = node.next return result
assert values(reverse_list(build([1, 2, 3, 4, 5]))) == [5, 4, 3, 2, 1]assert reverse_list(None) is Noneassert values(merge_two_lists(build([1, 4, 6]), build([2, 3, 7, 9]))) == [1, 2, 3, 4, 6, 7, 9]assert values(merge_two_lists(None, build([5]))) == [5]assert middle_node(build([1, 2, 3, 4, 5])).val == 3assert middle_node(build([1, 2, 3, 4])).val == 3print("ok: reverse_list, merge_two_lists and middle_node")using System;using System.Collections.Generic;using System.Linq;
class ListNode{ public int Val; public ListNode? Next;
public ListNode(int val = 0, ListNode? next = null) { Val = val; Next = next; }}
static class LinkedLists{ public static ListNode? ReverseList(ListNode? head) { // Reverse in place. Flipping current.Next cuts us off from the rest of the list, so we save // it in next first. previous is always the head of the part already reversed. O(n) time, // O(1) memory. ListNode? previous = null, current = head; while (current != null) { ListNode? next = current.Next; // save the rest before the link to it is lost current.Next = previous; // flip one arrow previous = current; current = next; } return previous; // the old tail is the new head }
public static ListNode? MergeTwoLists(ListNode? list1, ListNode? list2) { // Merge two sorted lists. The dummy node gives the result a head before we know which list // wins the first place, so there's no special case for the first node. O(n + m) for lists // of n and m nodes. var dummy = new ListNode(); var tail = dummy; while (list1 != null && list2 != null) { if (list1.Val <= list2.Val) // <= keeps equal values in their original order { tail.Next = list1; list1 = list1.Next; } else { tail.Next = list2; list2 = list2.Next; } tail = tail.Next; } tail.Next = list1 ?? list2; // what is left is already sorted, so link it whole return dummy.Next; }
public static ListNode? MiddleNode(ListNode? head) { // Fast moves two steps for each one slow moves, so when fast runs out, slow is halfway. // One pass and O(1) memory, without counting the length first. ListNode? slow = head, fast = head; while (fast != null && fast.Next != null) { slow = slow!.Next; fast = fast.Next.Next; } return slow; // for an even length, the second of the two middles }
static void Main() { Check(Values(ReverseList(Build(1, 2, 3, 4, 5))).SequenceEqual(new[] { 5, 4, 3, 2, 1 })); Check(ReverseList(null) == null); Check(Values(MergeTwoLists(Build(1, 4, 6), Build(2, 3, 7, 9))) .SequenceEqual(new[] { 1, 2, 3, 4, 6, 7, 9 })); Check(Values(MergeTwoLists(null, Build(5))).SequenceEqual(new[] { 5 })); Check(MiddleNode(Build(1, 2, 3, 4, 5))!.Val == 3); Check(MiddleNode(Build(1, 2, 3, 4))!.Val == 3); Console.WriteLine("ok: ReverseList, MergeTwoLists and MiddleNode"); }
static ListNode? Build(params int[] numbers) { var dummy = new ListNode(); var tail = dummy; foreach (int number in numbers) { tail.Next = new ListNode(number); tail = tail.Next; } return dummy.Next; }
static List<int> Values(ListNode? node) { var output = new List<int>(); for (; node != null; node = node.Next) output.Add(node.Val); return output; }
static void Check(bool ok) { if (!ok) throw new InvalidOperationException("check failed"); }}package main
import ( "fmt" "slices")
type ListNode struct { Val int Next *ListNode}
// Reverse in place. Flipping current.Next cuts us off from the rest of the list, so we save it in// next first. previous is always the head of the part already reversed. O(n) time for n nodes, O(1)// memory.func reverseList(head *ListNode) *ListNode { var previous *ListNode current := head for current != nil { next := current.Next // save the rest before the link to it is lost current.Next = previous // flip one arrow previous, current = current, next } return previous // the old tail is the new head}
// Merge two sorted lists. The dummy node gives the result a head before we know which list wins the// first place, so there's no special case for the first node. O(n + m) time for lists of n and m// nodes.func mergeTwoLists(list1, list2 *ListNode) *ListNode { dummy := &ListNode{} tail := dummy for list1 != nil && list2 != nil { if list1.Val <= list2.Val { // <= keeps equal values in their original order tail.Next, list1 = list1, list1.Next } else { tail.Next, list2 = list2, list2.Next } tail = tail.Next } // What is left is already sorted, so link it whole. if list1 != nil { tail.Next = list1 } else { tail.Next = list2 } return dummy.Next}
// Fast moves two steps for each one slow moves, so when fast runs out, slow is halfway.// One pass and O(1) memory, without counting the length first.func middleNode(head *ListNode) *ListNode { slow, fast := head, head for fast != nil && fast.Next != nil { slow, fast = slow.Next, fast.Next.Next } return slow // for an even length, the second of the two middles}
func build(numbers ...int) *ListNode { dummy := &ListNode{} tail := dummy for _, number := range numbers { tail.Next = &ListNode{Val: number} tail = tail.Next } return dummy.Next}
func values(node *ListNode) []int { var result []int for ; node != nil; node = node.Next { result = append(result, node.Val) } return result}
func main() { check(slices.Equal(values(reverseList(build(1, 2, 3, 4, 5))), []int{5, 4, 3, 2, 1})) check(reverseList(nil) == nil) merged := mergeTwoLists(build(1, 4, 6), build(2, 3, 7, 9)) check(slices.Equal(values(merged), []int{1, 2, 3, 4, 6, 7, 9})) check(slices.Equal(values(mergeTwoLists(nil, build(5))), []int{5})) check(middleNode(build(1, 2, 3, 4, 5)).Val == 3) check(middleNode(build(1, 2, 3, 4)).Val == 3) fmt.Println("ok: reverseList, mergeTwoLists and middleNode")}
func check(ok bool) { if !ok { panic("check failed") }}class ListNode { val: number; next: ListNode | null; constructor(val = 0, next: ListNode | null = null) { this.val = val; this.next = next; }}
function reverseList(head: ListNode | null): ListNode | null { // Reverse in place. Flipping current.next cuts us off from the rest of the list, so we save it in // next first. previous is always the head of the part already reversed. O(n) time for n nodes, // O(1) memory. let previous: ListNode | null = null; let current = head; while (current) { const next: ListNode | null = current.next; // save the rest before the link to it is lost current.next = previous; // flip one arrow previous = current; current = next; } return previous; // the old tail is the new head}
function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null { // Merge two sorted lists. The dummy node gives the result a head before we know which list wins // the first place, so there's no special case for the first node. O(n + m) time for lists of n // and m nodes. const dummy = new ListNode(); let tail = dummy; while (list1 && list2) { if (list1.val <= list2.val) { // <= keeps equal values in their original order tail = tail.next = list1; list1 = list1.next; } else { tail = tail.next = list2; list2 = list2.next; } } tail.next = list1 ?? list2; // what is left is already sorted, so link it whole return dummy.next;}
function middleNode(head: ListNode | null): ListNode | null { // Fast moves two steps for each one slow moves, so when fast runs out, slow is halfway. // One pass and O(1) memory, without counting the length first. let slow = head; let fast = head; while (fast && fast.next) { slow = slow!.next; fast = fast.next.next; } return slow; // for an even length, the second of the two middles}
const build = (...numbers: number[]): ListNode | null => { const dummy = new ListNode(); let tail = dummy; for (const number of numbers) tail = tail.next = new ListNode(number); return dummy.next;};const values = (node: ListNode | null): number[] => { const result: number[] = []; for (; node; node = node.next) result.push(node.val); return result;};const check = (ok: boolean) => { if (!ok) throw new Error("check failed");};check(values(reverseList(build(1, 2, 3, 4, 5))).join() === "5,4,3,2,1");check(reverseList(null) === null);check(values(mergeTwoLists(build(1, 4, 6), build(2, 3, 7, 9))).join() === "1,2,3,4,6,7,9");check(values(mergeTwoLists(null, build(5))).join() === "5");check(middleNode(build(1, 2, 3, 4, 5))!.val === 3);check(middleNode(build(1, 2, 3, 4))!.val === 3);console.log("ok: reverseList, mergeTwoLists and middleNode");Look at the loop conditions. Reverse runs while current, because every node must flip. Middle runs
while fast and fast.next, because fast reads two nodes ahead and needs both. Get one of these
wrong and you either skip the last node or read a pointer that is null.
Variations
Section titled “Variations”Remove the nth node from the end
Section titled “Remove the nth node from the end”Move a lead pointer n steps ahead (n is position_from_end in code). Then move
lead and a trail pointer together until
lead reaches the last node. The gap between them stays n, so trail stops just before the node to
remove. Start both at a dummy head. Then removing the real head is not a special case.
Reorder and palindrome checks
Section titled “Reorder and palindrome checks”Reorder List turns 1 → 2 → 3 → 4 → 5 into 1 → 5 → 2 → 4 → 3. It’s three moves from this page in a row. Find the middle with fast and slow. Reverse the second half. Then weave the two halves together, one node from each. Palindrome Linked List uses the same first two moves, then compares the halves node by node. Both run in O(n) time and O(1) memory.
Reverse a part of the list
Section titled “Reverse a part of the list”To reverse nodes from position left to right, walk to the node just before left and keep it.
Reverse the next right - left + 1 nodes with the usual loop. Then connect both ends back. The
node before the part points to the new start, and the old start points to what came after. The
k-group version repeats this for every block of k nodes.
Cycles
Section titled “Cycles”Fast and slow meet inside a loop if there is one. This is explained on the two pointers page. To find where the loop starts, move one pointer back to the head after they meet. Then move both one step at a time. They meet again at the start of the loop.
Complexity
Section titled “Complexity”| Time | Extra memory | |
|---|---|---|
| Reach the kth node | O(k) | O(1) |
| Reverse, find middle, find loop | O(n) | O(1) |
| Merge two sorted lists | O(n + m) | O(1) |
| Reverse recursively | O(n) | O(n) stack |
| Merge k lists with a heap | O(N log k) | O(k) |
I’ll keep two chains: previous is the reversed part, and current is what’s left. Each step I save current.next, point current back at previous, and move both forward. Every node moves once, so it’s O(n) time and O(1) memory.
Interview questions
Section titled “Interview questions”Interviewer asksWhy use a dummy node? Couldn't you just handle the head separately?
You can, but it takes an extra branch for “the result is empty so far”. That branch is where bugs
hide. With a dummy node, the tail always exists, so every node is added the same way. It costs one
extra node. Return dummy.next, never dummy.
Interviewer asksCan you reverse the list recursively? What does it cost?
Yes. Reverse everything after the head, then make the old second node point back at the head, and set the head’s next to null. It is O(n) time, but the call stack holds n frames. That’s O(n) memory, and a very long list can overflow the stack. The loop version is safer.
Interviewer asksFor an even length list, which middle does fast and slow return?
With while fast and fast.next, slow stops on the second of the two middles. For 1, 2, 3, 4 it
returns 3. To get the first middle, start fast one step ahead, at head.next. Pick the one your
next step needs. Reorder List splits after the first middle, so the first half is not shorter than
the second.
Practice
Section titled “Practice”10 problems on LeetCode: 4 easy, 5 medium, 1 hard
- Reverse Linked List(opens LeetCode in a new tab)Easy
Save the next node before you flip the arrow that leads to it.
- Merge Two Sorted Lists(opens LeetCode in a new tab)Easy
A dummy head means the first node is not a special case.
- Middle of the Linked List(opens LeetCode in a new tab)Easy
One pointer moves twice as fast as the other.
- Palindrome Linked List(opens LeetCode in a new tab)Easy
Find the middle, reverse the second half, then compare the halves.
- Remove Nth Node From End of List(opens LeetCode in a new tab)Medium
Start one pointer n nodes ahead, and the gap finds the node for you.
- Reorder List(opens LeetCode in a new tab)Medium
Three moves you already know: find the middle, reverse, merge.
- Add Two Numbers(opens LeetCode in a new tab)Medium
Walk both lists together and carry, like adding by hand.
- Linked List Cycle II(opens LeetCode in a new tab)Medium
After fast and slow meet, the distance to the cycle start is the same from both ends.
- Copy List with Random Pointer(opens LeetCode in a new tab)Medium
A map from each old node to its copy fixes the random links in a second pass.
- Reverse Nodes in k-Group(opens LeetCode in a new tab)Hard
Reverse one block of k at a time and reconnect it to the blocks around it.
Ticks are saved in this browser only. Clear your site data and they're gone.
Four of these are solved step by step on this site, as part of the Blind 75: Reverse Linked List, Merge Two Sorted Lists, Remove Nth Node From End of List and Reorder List. Two more use the same moves: Linked List Cycle and Merge k Sorted Lists.
- Use it when the input is a linked list and you must work in place, find a middle, find a loop, reorder or merge.
- Four moves: dummy head, fast and slow, reverse in place, merge.
- The rule: save a pointer before you overwrite the link to it.
- Cost: O(n) time and O(1) memory for almost everything. Recursion adds O(n) stack.
- Watch for: losing the rest of the list, null checks in
while fast and fast.next, and returning the dummy instead ofdummy.next.
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.