LinkedList In-place Reversal
Reverse a linked list (or just a piece of it) by flipping each arrow to point backwards, one node at a time, using three pointers and no extra memory.
1. What is this pattern for?
prev_node), the one you're turning now (current), and the next one in line (next_node), so you don't lose the rest of the line when current lets go.A singly linked list is a chain of nodes where each node knows only its next. You can't index into it or walk backwards. The in-place reversal pattern rewires the next pointers so the chain points the other way, in O(n) time and O(1) extra space (no copying into an array).
It's the building block for: reversing the whole list, reversing positions left..right, reversing every group of k, swapping pairs, and "compare the first half with the reversed second half" problems (palindrome, twin sums, reorder).
2. How it works (bisected)
- Set up.
prev_node = None(the new tail will point at nothing),current = head. - Save.
next_node = current.next. Do this first, because the next line destroys your only link to the rest of the list. - Flip.
current.next = prev_node. The arrow now points backwards. - Advance both.
prev_node = current, thencurrent = next_node. Repeat steps 2–4 untilcurrentisNone. - Return
prev_node: it's the old tail, now the new head. - Level up: reverse just a part. Use a dummy node before the head so "the node before the part" always exists (even when the part starts at the head). Remember the node just before the part and the part's first node (it'll become the part's tail), reverse the part, then stitch both ends back.
Naming note: the code on this page uses the long names prev_node, current and next_node. The animations below have little room for labels, so they shorten them to prev, cur and nxt. They are the same three pointers.
3. Seeing a problem: how to spot In-place Reversal
Trigger words
- "reverse the linked list", "reverse from position left to right"
- "reverse nodes in k-group", "swap every two adjacent nodes"
- "in place", "O(1) extra memory", "do not modify the values, only the nodes"
- "palindrome linked list", "twin sum", "reorder list", "add two numbers II" (when the digits are stored in an inconvenient order)
Shape of the input
- A singly linked list (
ListNodewithvalandnext). - You need to traverse it in the opposite order, or reorder blocks of nodes.
- Brute-force smell: "copy all values into a Python list, reverse/re-arrange, write them back". That's
O(n)extra space, and often forbidden ("don't change values").
prev_node / current / next_node.4. When to use it, and when not to
| Use In-place Reversal when… | Prefer something else when… |
|---|---|
You must reverse all or part of a linked list in O(1) space | Space doesn't matter and clarity does: push nodes onto a stack or into an array |
| Comparing the first half with the second half backwards (palindrome, twin sum) | You only need the middle or a cycle: that's Fast & Slow Pointers (often used together: find middle, then reverse) |
| Reordering blocks of nodes (k-group, swap pairs) | You have an array: reversing a slice is just Two Pointers swapping from both ends |
| Reversal must not allocate new nodes | You're merging sorted lists: use a dummy + splice (or a heap for k lists, see Top-K) |
Cost: O(n) time (each node's arrow is flipped once), O(1) extra space. The recursive version is also O(n) time but uses O(n) call-stack space.
5. The Python template (memorize this)
# A) Reverse the whole list with three pointers.
def reverse(head):
# prev_node = the front of the part we have ALREADY reversed.
# Nothing is reversed yet, so it starts as None
# (the old head becomes the new tail, and a tail points at None).
prev_node = None
# current = the node whose arrow we are about to flip.
current = head
# Keep going until we walk off the end of the list.
while current is not None:
# 1. SAVE the rest of the list. The next line overwrites
# current.next, and without this we would lose the rest forever.
next_node = current.next
# 2. FLIP the arrow: current now points backwards.
current.next = prev_node
# 3. STEP prev_node forward: current is the new front of the reversed part.
prev_node = current
# 4. STEP current forward: move on to the saved rest of the list.
current = next_node
# current is None now. prev_node is the old tail, which is the new head.
return prev_node
# B) Reverse only positions left..right (1-indexed) with a dummy node
# and "head insertion".
def reverse_between(head, left, right):
# A dummy node in front of head guarantees that a node BEFORE the part
# always exists, even when left == 1.
dummy = ListNode(0, head)
# Walk left - 1 steps so that 'before' stops just before position left.
before = dummy
for step in range(left - 1):
before = before.next
# tail = the first node of the part. It never moves itself;
# the other nodes jump over it, so it ends up LAST in the part.
tail = before.next
# right - left moves are enough to reverse the part.
for move in range(right - left):
# The node right after tail is the one we move this time.
moved = tail.next
# Unhook it: tail now skips over it.
tail.next = moved.next
# Insert it at the FRONT of the part, right after 'before'.
moved.next = before.next
before.next = moved
# dummy.next is the real head (it changed if left was 1).
return dummy.next
The same template, explained for a beginner:
What the code is doing, line by line:
- Template A, set up:
prev_node = Nonebecause nothing has been reversed yet (and the old head, which becomes the new tail, must point atNone).current = headis the first node whose arrow we will flip. while current is not None:repeat until we have walked off the end of the list.- Save:
next_node = current.nextremembers the rest of the list. This must come first, because the next line overwritescurrent.next. - Flip:
current.next = prev_nodeturns the arrow around, socurrentnow points back at the already-reversed part. - Step, step:
prev_node = current(the reversed part grew by one node) andcurrent = next_node(move on to the saved rest). return prev_node: whencurrentisNone,prev_nodeis the old last node, which is the new head.- Template B, set up:
dummy = ListNode(0, head)puts a fake node in front, so there is always a node before the part, even whenleft == 1. for step in range(left - 1): before = before.nextwalks to the node just before positionleft.tail = before.nextis the first node of the part. It never moves itself: the other nodes jump in front of it, so it ends up last.for move in range(right - left):each move takesmoved = tail.next, unhooks it (tail.next = moved.next) and inserts it at the front of the part (moved.next = before.next, thenbefore.next = moved).return dummy.next: the real head (it is a different node ifleftwas 1).
Dry run of template A on head = 1 → 2 → 3 (each row is the state after one save/flip/step/step round):
| Step | current (flipped) | next_node (saved) | reversed part (from prev_node) | rest (from current) |
|---|---|---|---|---|
| start | — | — | None | 1 → 2 → 3 |
| 1 | 1 | 2 | 1 | 2 → 3 |
| 2 | 2 | 3 | 2 → 1 | 3 |
| 3 | 3 | None | 3 → 2 → 1 | None |
| end | None | — | 3 → 2 → 1 | None → loop stops, return prev_node = 3 |
Dry run of template B on head = 1 → 2 → 3 → 4 → 5 → 6, left = 2, right = 5 (the same input as the second animation in section 2):
| Step | before | tail | moved | list (from dummy.next) |
|---|---|---|---|---|
| setup | 1 | 2 | — | 1 → 2 → 3 → 4 → 5 → 6 |
| move 1 | 1 | 2 | 3 | 1 → 3 → 2 → 4 → 5 → 6 |
| move 2 | 1 | 2 | 4 | 1 → 4 → 3 → 2 → 5 → 6 |
| move 3 | 1 | 2 | 5 | 1 → 5 → 4 → 3 → 2 → 6 |
| return | dummy.next → 1 → 5 → 4 → 3 → 2 → 6 |
6. How to remember it forever
next_node = current.next → current.next = prev_node → prev_node = current → current = next_node. Each line's left side is the previous line's right side, like a chain.How this hook works: what “Save, flip, step, step” means
What the slogan means, phrase by phrase
- “Save”
next_node = current.next: remember the rest of the line before you let go of it.- “flip”
current.next = prev_node: turn this arrow around so it points backwards.- “step”
prev_node = current: the node you just flipped is now the front of the reversed part.- “step”
current = next_node: move on to the part you saved.
Why this picture fits the pattern: Reversing a list is four pointer moves repeated, and the order is everything: flip before you save and the rest of the list is lost. The slogan is the four lines in order, one word per line.
The story → the code
| In the story | In the code |
|---|---|
| The dancer you just turned | prev_node |
| The dancer you're turning now | current |
| The next dancer in line | next_node |
| Save, flip, step, step | the four lines inside while current is not None |
| The new front of the line | return prev_node |
Watch it on numbers
Reverse 1 → 2 → 3. Start with prev_node = None and current = 1.
- Save 2. Flip 1 → None. Step:
prev_node = 1,current = 2. Reversed so far: 1. - Save 3. Flip 2 → 1. Step:
prev_node = 2,current = 3. Reversed so far: 2 → 1. - Save None. Flip 3 → 2. Step:
prev_node = 3,current = None. Reversed: 3 → 2 → 1. - Notice the chain: each line's left side is the previous line's right side (
next_node,current.next,prev_node,current).
The trap it saves you from
Skipping "save" (the rest of the list is lost the moment you flip), or returning head, which is now the tail. The new head is prev_node.
It still works when…
Reverse a sublist (LeetCode 92) or k nodes at a time: put a dummy node in front, walk to the node before the section, run the same four lines k times, then reconnect both ends.
Picture
A row of dominoes with arrows. Your hand moves left to right, turning each arrow around. Behind your hand is a reversed list (prev_node is its head); ahead is the untouched rest (next_node).
Chant
"Save the next, point back, prev_node steps up, current steps on."
Dummy rule
"If the head might change, start with a dummy." Then return dummy.next.
7. LeetCode problems, solved step by step
Hard 25. Reverse Nodes in k-Group Google Meta Amazon Cisco Microsoft
Problem: Reverse the nodes of the list k at a time. If the last group has fewer than k nodes, leave it as it is. Only change links, not values.
head = [1,2,3,4,5], k = 2[2,1,4,3,5]head = [1,2,3,4,5], k = 3[3,2,1,4,5]Constraints: The number of nodes in the list is n. · 1 ≤ k ≤ n ≤ 5000 · 0 ≤ Node.val ≤ 1000
Hint 1
Handle the list one group of k nodes at a time. Check that a full group exists before touching it.
Hint 2
Reverse the group with prev/cur/next, then reconnect: the node before the group points to the new group head, and the old group head (now its tail) points to what came after.
From brute force to the pattern: Copying values into an array, reversing chunks and rebuilding uses O(n) space. Reversing each full group in place with careful reconnection is O(n) time and O(1) space; a dummy node avoids special-casing the first group.
Spot it: "reverse" + "k at a time" + "only nodes may be changed": repeated sub-list reversal with stitching.
Key insight: keep group_prev (the node before the group). First check that k nodes exist (find kth). Reverse the group with prev_node initialised to group_next instead of None: that way the group's old first node automatically points to the rest of the list. Then hook group_prev.next = kth and move group_prev to the old first node (now the group's tail).
Approach:
- Make a dummy before
head;group_prev = dummy. - Walk
ksteps fromgroup_prevto findkth. If you hitNone, returndummy.next. - Save
group_next = kth.next. Reverse theknodes withprev_nodestarting atgroup_next. - Stitch:
group_prev.next = kth(the new front). The old first node is now the group's tail, so it becomes the nextgroup_prev. - Repeat.
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# The head changes after the first group, so put a dummy in front.
dummy = ListNode(0, head)
# group_prev = the node just BEFORE the group we are working on.
group_prev = dummy
while True:
# --- 1) Is there a full group of k nodes? ---
# Walk k steps from group_prev. We land on the k-th node of the group.
kth = group_prev
for step in range(k):
kth = kth.next
if kth is None:
# Fewer than k nodes are left: leave them as they are.
return dummy.next
# The first node AFTER this group (it may be None).
group_next = kth.next
# The group's first node. After reversing, it will be the group's LAST node.
group_first = group_prev.next
# --- 2) Reverse the k nodes of this group (save, flip, step, step). ---
# Trick: prev_node starts at group_next instead of None, so the old
# first node automatically ends up pointing at the rest of the list.
prev_node = group_next
current = group_first
while current is not group_next:
next_node = current.next # save the rest
current.next = prev_node # flip the arrow
prev_node = current # step prev_node
current = next_node # step current
# --- 3) Stitch the front of the group back in. ---
# kth was the group's last node; now it is the group's first node.
group_prev.next = kth
# The old first node is now the group's tail, so it is
# the node BEFORE the next group.
group_prev = group_first
What the code is doing, line by line:
dummy = ListNode(0, head)andgroup_prev = dummy: the head will change after the first group, so we keep a fake node in front.group_prevalways sits just before the group we are working on.while True:handle one group per round. The only way out is thereturninside.- Check for a full group: starting at
group_prev, thefor step in range(k)loop walksknodes. If it falls off the end (kth is None), fewer thanknodes are left, so we stop and returndummy.nextwithout touching them. group_next = kth.nextis the first node after the group;group_first = group_prev.nextis the group's first node (it will become the group's last node).- Reverse the group: the usual save / flip / step / step loop, with one trick:
prev_nodestarts atgroup_nextinstead ofNone. So whengroup_firstis flipped, it points straight at the rest of the list and the back end is already stitched. The loop stops whencurrentreachesgroup_next. group_prev.next = kthstitches the front: the node before the group now points at the old last node, which is the new first node.group_prev = group_first: the old first node is now the group's tail, so it is the node before the next group.
Dry run on LeetCode example head = [1,2,3,4,5], k = 2 (expected [2,1,4,3,5]):
| Round | group_prev | kth | group_next | list after stitching |
|---|---|---|---|---|
| group 1 | dummy | 2 | 3 | 2 → 1 → 3 → 4 → 5 (new group_prev = 1) |
| group 2 | 1 | 4 | 5 | 2 → 1 → 4 → 3 → 5 (new group_prev = 3) |
| check 3 | 3 | None (ran out) | — | fewer than k nodes left → return 2 → 1 → 4 → 3 → 5 |
Edge cases to test:
k = 1→ unchanged.- Length not a multiple of
k: the last partial group stays as is. kequal to the length → the whole list reversed.
Complexity: O(n) time (each node visited twice), O(1) space.
Medium 92. Reverse Linked List II Start here Google Amazon Microsoft
Problem: Reverse only positions left..right (1-indexed) of the list, in one pass.
head = [1,2,3,4,5], left = 2, right = 4[1,4,3,2,5]head = [5], left = 1, right = 1[5]Constraints: The number of nodes in the list is n. · 1 ≤ n ≤ 500 · -500 ≤ Node.val ≤ 500 · 1 ≤ left ≤ right ≤ n
Hint 1
Walk to the node just before position left.
Hint 2
Reverse exactly right − left + 1 nodes, then reconnect both ends of the reversed piece.
From brute force to the pattern: Copying the values between left and right into an array and writing them back reversed uses O(n) space. Reversing in place in a single pass, with a dummy node for left = 1, is O(n) time and O(1) space.
Spot it: "reverse from position left to right". Partial reversal: needs a dummy (left may be 1) and stitching. The second visualizer above traces it.
Key insight: "head insertion": the part's first node (tail) stays put while each following node is unhooked and moved to the front of the part. After right − left moves, the part is reversed and both ends are already connected.
Approach:
- Create a dummy before
headand walkleft − 1steps to reachbefore. tail = before.next: the part's first node, which will end up last.- Repeat
right − lefttimes: unhookmoved = tail.nextand insert it right afterbefore. - Return
dummy.next.
class Solution:
def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
# A dummy node in front of head: a node before the part always exists,
# even when left == 1.
dummy = ListNode(0, head)
# Walk left - 1 steps so that 'before' stops just before position left.
before = dummy
for step in range(left - 1):
before = before.next
# tail = first node of the part. It stays where it is while the other
# nodes are moved in front of it, so it ends up last.
tail = before.next
# Do right - left moves.
for move in range(right - left):
# The node right after tail is the one we move.
moved = tail.next
# Unhook it: tail skips over it.
tail.next = moved.next
# Insert it at the front of the part (right after 'before').
moved.next = before.next
before.next = moved
# dummy.next is the real head (it changed if left was 1).
return dummy.next
What the code is doing, line by line:
dummy = ListNode(0, head): a fake node before the head, so the node before the part always exists (needed whenleft == 1).before = dummy, thenfor step in range(left - 1): before = before.nextwalks to the node just before positionleft.tail = before.next: the first node of the part. It stays put while the nodes after it are moved in front of it, so it ends up last.for move in range(right - left):a part ofright - left + 1nodes needsright - leftmoves.moved = tail.nextpicks the node right aftertail;tail.next = moved.nextunhooks it (tail skips over it).moved.next = before.nextandbefore.next = movedinsert it at the front of the part, right afterbefore.return dummy.next: the real head of the list.
Dry run on LeetCode example head = [1,2,3,4,5], left = 2, right = 4 (expected [1,4,3,2,5]):
| Step | before | tail | moved | list (from dummy.next) |
|---|---|---|---|---|
| setup | 1 | 2 | — | 1 → 2 → 3 → 4 → 5 |
| move 1 | 1 | 2 | 3 | 1 → 3 → 2 → 4 → 5 |
| move 2 | 1 | 2 | 4 | 1 → 4 → 3 → 2 → 5 |
| return | dummy.next → 1 → 4 → 3 → 2 → 5 |
Edge cases to test:
left == right→ unchanged.left = 1(the head changes).rightis the last node.
Complexity: O(n) time, O(1) space.
Medium 24. Swap Nodes in Pairs Google Meta Amazon Microsoft
Problem: Swap every two adjacent nodes (1→2→3→4 becomes 2→1→4→3) without changing values.
head = [1,2,3,4][2,1,4,3]head = [][]Constraints: The number of nodes in the list is in the range [0, 100]. · 0 ≤ Node.val ≤ 100
Hint 1
Swap nodes, not values: re-point three next pointers per pair.
Hint 2
Keep a pointer to the node before each pair (a dummy for the first pair), then advance two at a time.
From brute force to the pattern: Swapping the values is simple but is not what's asked (nodes must move). Re-linking each pair in place is O(n) time and O(1) space.
Spot it: it's k-group reversal with k = 2, small enough to write the three re-links by hand.
Key insight: with prev_node → first → second → rest, set first.next = rest, second.next = first, prev_node.next = second. Then first is the new prev_node.
Approach:
dummy → head;prev_node = dummy.- While two nodes
first,secondfollowprev_node:first.next = second.next,second.next = first,prev_node.next = second. - Move
prev_nodetofirst(now the second node of the pair). - Return
dummy.next.
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# The head changes (the 2nd node becomes the 1st), so use a dummy.
dummy = ListNode(0, head)
# prev_node = the node just BEFORE the pair we are about to swap.
prev_node = dummy
# A pair needs two nodes after prev_node.
while prev_node.next is not None and prev_node.next.next is not None:
# Name the two nodes of the pair.
first = prev_node.next
second = prev_node.next.next
# first skips ahead to whatever comes after the pair.
first.next = second.next
# second points back to first.
second.next = first
# The node before the pair now points to second (the new front).
prev_node.next = second
# first is now the back of the pair, so it is the node
# before the next pair.
prev_node = first
return dummy.next
What the code is doing, line by line:
dummy = ListNode(0, head): the second node will become the head, so we use a fake node in front and returndummy.nextat the end.prev_node = dummy: the node just before the pair we are about to swap.while prev_node.next is not None and prev_node.next.next is not None:only swap if two nodes followprev_node. A single leftover node stays where it is.first = prev_node.nextandsecond = prev_node.next.nextgive the two nodes of the pair names.first.next = second.next: first skips ahead to whatever comes after the pair.second.next = first: second now points back at first.prev_node.next = second: the node before the pair now points at second, the new front of the pair.prev_node = first: first is now the back of the pair, so it is the node before the next pair.
Dry run on LeetCode example head = [1,2,3,4] (expected [2,1,4,3]):
| Pair | prev_node (before pair) | first | second | list after the swap |
|---|---|---|---|---|
| start | dummy | — | — | 1 → 2 → 3 → 4 |
| 1 | dummy | 1 | 2 | 2 → 1 → 3 → 4 (prev_node → 1) |
| 2 | 1 | 3 | 4 | 2 → 1 → 4 → 3 (prev_node → 3) |
| stop | 3 | fewer than 2 nodes after prev_node → return 2 → 1 → 4 → 3 |
Edge cases to test:
- Empty list or a single node → unchanged.
- Odd length: the last node stays put.
- Exactly two nodes.
Complexity: O(n) time, O(1) space.
Medium 2130. Maximum Twin Sum of a Linked List Amazon
Problem: In a list of even length n, node i and node n−1−i are twins. Return the largest twin sum.
head = [5,4,2,1]6head = [4,2,2,3]7Constraints: The number of nodes in the list is an even integer in the range [2, 10⁵]. · 1 ≤ Node.val ≤ 10⁵
Hint 1
Node i pairs with node n − 1 − i: the first half pairs with the second half read backwards.
Hint 2
Find the middle, reverse the second half, then walk both halves together summing pairs.
From brute force to the pattern: Copying values to an array and summing a[i] + a[n − 1 − i] is O(n) space. Middle + reverse + walk does it in place with O(1) space.
Spot it: you need to walk the first half forwards and the second half backwards at the same time. Reverse the second half in place.
Key insight: find the start of the second half with fast & slow, reverse from there, then walk both halves together.
Approach:
- Fast/slow puts
slowon the first node of the second half. - Reverse the second half starting at
slow. - Walk one pointer from
headand one from the reversed half together, adding twin values. - Return the maximum sum.
class Solution:
def pairSum(self, head: Optional[ListNode]) -> int:
# --- 1) Find the first node of the second half (fast & slow). ---
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next # slow moves 1 step
fast = fast.next.next # fast moves 2 steps
# slow is now the first node of the second half.
# --- 2) Reverse the second half, starting at slow. ---
prev_node = None
current = slow
while current is not None:
next_node = current.next # save
current.next = prev_node # flip
prev_node = current # step
current = next_node # step
# prev_node is the head of the reversed second half (the old last node).
# --- 3) Walk both halves together and add the twins. ---
best = 0
front = head # walks the first half forwards
back = prev_node # walks the second half "backwards"
while back is not None:
twin_sum = front.val + back.val
# Keep the biggest sum seen so far.
if twin_sum > best:
best = twin_sum
front = front.next
back = back.next
return best
What the code is doing, line by line:
- Phase 1:
slowandfastboth start at the head. Each roundslowmoves 1 node andfastmoves 2. Whenfastcan't move two more nodes,slowis on the first node of the second half (the list has even length). - Phase 2: reverse the second half with save / flip / step / step, starting at
slow. Afterwardsprev_nodeis the old last node, which is now the head of the reversed second half. - Phase 3:
frontwalks the first half from the head;backwalks the reversed second half. Nodeimeets its twinn − 1 − iat the same time. twin_sum = front.val + back.val, and theif twin_sum > bestblock keeps the largest sum seen so far.- Both pointers step forward; the loop ends when
backruns out. Returnbest.
Dry run on LeetCode example head = [5,4,2,1] (expected 6):
| Phase | Variables | What happened |
|---|---|---|
| 1 find middle | slow = 5, fast = 5 | start |
| 1 find middle | slow = 4, fast = 2 | slow +1, fast +2 |
| 1 find middle | slow = 2, fast = None | slow +1, fast +2 |
| 1 find middle | fast can't move 2 more → second half starts at 2 | |
| 2 reverse | flipped 2, next_node = 1 | reversed half: 2 |
| 2 reverse | flipped 1, next_node = None | reversed half: 1 → 2 |
| 3 compare | front = 5, back = 1 | twin_sum = 6, best = 6 |
| 3 compare | front = 4, back = 2 | twin_sum = 6, best = 6 |
| return | back = None | best = 6 |
Edge cases to test:
- Two nodes → their sum.
- All equal values.
- The maximum pair is in the middle.
Complexity: O(n) time, O(1) space.
Easy 206. Reverse Linked List (iterative + recursive) Start here Google Meta Amazon Cisco Microsoft Apple
Problem: Reverse the entire list and return the new head.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,2,3,4,5][5,4,3,2,1]head = [1,2][2,1]Constraints: The number of nodes in the list is the range [0, 5000]. · -5000 ≤ Node.val ≤ 5000
Hint 1
Each node's next must point backwards. What do you need to remember before changing it?
Hint 2
Keep prev and cur; save cur.next, point cur.next to prev, then step both forward.
From brute force to the pattern: Pushing the values on a stack and rebuilding uses O(n) extra space. Re-pointing every next in one pass is O(n) time and O(1) space (the recursive version uses O(n) stack).
Spot it: template A word for word. Interviewers often ask for the recursive version too.
Recursive insight: reverse everything after head; now head.next is the tail of that reversed part, so hang head after it: head.next.next = head, and cut head.next = None.
Approach:
- Iterative:
prev_node = None,current = head; repeat save / flip / step / step untilcurrentisNone; returnprev_node. - Recursive base case: an empty or single-node list is its own reverse.
- Reverse the rest;
head.nextis now the last node of that reversed part. - Call the node after head
second; setsecond.next = head(that ishead.next.next = head) andhead.next = None, then return the head found by the recursion.
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Iterative version: template A.
prev_node = None # nothing reversed yet
current = head # start flipping at the head
while current is not None:
next_node = current.next # 1. save the rest of the list
current.next = prev_node # 2. flip the arrow backwards
prev_node = current # 3. step prev_node forward
current = next_node # 4. step current forward
# The old tail is the new head.
return prev_node
def reverseListRecursive(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Base case: an empty list or a single node is already reversed.
if head is None or head.next is None:
return head
# Reverse everything AFTER head. new_head is the old last node.
new_head = self.reverseListRecursive(head.next)
# head.next still points to the node that came right after head.
# That node is now the TAIL of the reversed rest.
second = head.next
# Hang head after it: the node after me now points back to me.
second.next = head
# head is the new tail (for now), so it must point at nothing.
head.next = None
return new_head
What the code is doing, line by line:
- reverseList (iterative):
prev_node = None(nothing reversed yet) andcurrent = head. - Inside
while current is not None:do the four lines in order: savenext_node = current.next, flipcurrent.next = prev_node, stepprev_node = current, stepcurrent = next_node. - When
currentbecomesNone,prev_nodeis the old tail = new head, so return it. - reverseListRecursive: base case: if the list is empty or has one node, it is already reversed, so return
head. new_head = self.reverseListRecursive(head.next)trusts the recursion to reverse everything afterhead.new_headis the old last node and never changes on the way back up.second = head.next: this node was right afterhead, and after the recursive call it is the tail of the reversed rest.second.next = headhangsheadafter it;head.next = Nonemakesheadthe new tail (otherwise we would get a 2-node cycle).- Return
new_headso every level passes the same new head upwards.
Dry run of reverseList on LeetCode example head = [1,2,3,4,5] (expected [5,4,3,2,1]):
| Step | current (flipped) | next_node (saved) | reversed part (from prev_node) | rest (from current) |
|---|---|---|---|---|
| start | — | — | None | 1 → 2 → 3 → 4 → 5 |
| 1 | 1 | 2 | 1 | 2 → 3 → 4 → 5 |
| 2 | 2 | 3 | 2 → 1 | 3 → 4 → 5 |
| 3 | 3 | 4 | 3 → 2 → 1 | 4 → 5 |
| 4 | 4 | 5 | 4 → 3 → 2 → 1 | 5 |
| 5 | 5 | None | 5 → 4 → 3 → 2 → 1 | None |
| end | None | — | 5 → 4 → 3 → 2 → 1 | None → loop stops, return prev_node = 5 |
Dry run of reverseListRecursive on head = [1,2,3]. The calls go down to depth 3 first; the rows show them finishing from the deepest call back up:
| Call depth | head | What happens when this call finishes | list from new_head |
|---|---|---|---|
| 3 | 3 | base case: return 3 | 3 |
| 2 | 2 | second = 3; 3.next = 2; 2.next = None; return 3 | 3 → 2 |
| 1 | 1 | second = 2; 2.next = 1; 1.next = None; return 3 | 3 → 2 → 1 |
Edge cases to test:
- Empty list →
None. - A single node.
- Two nodes.
Complexity: O(n) time; O(1) space iterative, O(n) stack recursive.
8. Common mistakes
current.next = prev_node before next_node = current.next loses the rest of the list forever.head or currentAfter the loop, current is None and head is the new tail. Return prev_node.left = 1, there's no "node before". A dummy removes that special case, so return dummy.next.k nodes exist before reversing.9. Self-check quiz
Inside the reversal loop, which line must come first?
After while current is not None: finishes, which variable holds the new head?
current is None; prev_node is the last node processed, the old tail.You're reversing positions 1..3. What makes the code free of special cases?
dummy.next.Check whether a linked list is a palindrome using O(1) extra space.
10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 206 | Reverse Linked List | Easy | Warm-up: template A, then recursive | |
| 21 | Merge Two Sorted Lists | Easy | Re-link existing nodes behind a dummy; no new nodes | |
| 83 | Remove Duplicates from Sorted List | Easy | Skip the next node while it repeats the current value | |
| 234 | Palindrome Linked List (O(1) space) | Easy | Middle + reverse + compare | |
| 92 | Reverse Linked List II | Medium | Dummy + head insertion | |
| 24 | Swap Nodes in Pairs | Medium | k-group with k = 2 | |
| 2130 | Maximum Twin Sum of a Linked List | Medium | Reverse the second half | |
| 143 | Reorder List | Medium | Middle + reverse + weave | |
| 61 | Rotate List | Medium | Close into a ring, cut at n − k | |
| 445 | Add Two Numbers II | Medium | Reverse both, add with carry, prepend each digit | |
| 2074 | Reverse Nodes in Even Length Groups | Medium | Group sizes 1, 2, 3…; reverse only even ones | |
| 2 | Add Two Numbers | Medium | Walk both lists at once with a carry; build the answer behind a dummy | |
| 82 | Remove Duplicates from Sorted List II | Medium | Delete the whole run of a repeated value; prev stays before it | |
| 138 | Copy List with Random Pointer | Medium | Map every old node to its copy, then wire next and random | |
| 2807 | Insert Greatest Common Divisors in Linked List | Medium | Splice a new node between every adjacent pair | |
| 86 | Partition List | Medium | Two dummy-headed lists (smaller / rest), then link them | |
| 109 | Convert Sorted List to Binary Search Tree | Medium | In-order simulation: build left subtree, consume the list node, build right subtree | |
| 147 | Insertion Sort List | Medium | Splice each node into a growing sorted list behind a dummy | |
| 237 | Delete Node in a Linked List | Medium | You can't reach the previous node, so become the next node and delete it instead | |
| 328 | Odd Even Linked List | Medium | Weave two chains (odd positions, even positions), then attach evens after odds | |
| 707 | Design Linked List | Medium | A sentinel head plus a size counter keeps every operation simple | |
| 2058 | Find the Minimum and Maximum Number of Nodes Between Critical Points | Medium | Walk with prev/cur/next; track the first, the previous and the latest critical index | |
| 708 | Insert into a Sorted Circular Linked List | Medium | Walk once around: insert between prev ≤ x ≤ cur, or at the wrap point (max → min) if x is a new max or min | |
| 1721 | Swapping Nodes in a Linked List | Medium | Walk k steps for the first node; a second pointer started then reaches the k-th from the end | |
| 25 | Reverse Nodes in k-Group | Hard | Check k nodes, reverse with prev_node = group_next, stitch |
Worked solutions for the practice list
Problems already solved in section 7 link back up there. Every other problem from the table is solved here.
Easy 234. Palindrome Linked List (O(1) space) Google Meta Nvidia Amazon Microsoft
Problem: Do the list's values read the same forwards and backwards, using only O(1) extra space?
head = [1,2,2,1]truehead = [1,2]falseConstraints: The number of nodes in the list is in the range [1, 10⁵]. · 0 ≤ Node.val ≤ 9
Hint 1
Copying the values into an array and comparing with its reverse uses O(n) space. How can you avoid that?
Hint 2
Find the middle, reverse the second half in place, then compare the two halves node by node.
From brute force to the pattern: An array copy or a stack gives O(n) space. Middle-finding plus an in-place reversal of the second half compares the halves with O(1) extra space (and you can reverse it back afterwards).
Spot it: Comparing the front with the back means reading the second half backwards. Reverse it in place instead of copying into an array.
Approach:
- Find the start of the second half with fast & slow.
- Reverse the second half with save / flip / step / step.
- Walk from
headand from the reversed half together, comparing values. - Any mismatch →
False; otherwiseTrue.
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# --- 1) Move slow to the start of the second half (fast & slow). ---
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next # 1 step
fast = fast.next.next # 2 steps
# --- 2) Reverse the second half in place. ---
prev_node = None
current = slow
while current is not None:
next_node = current.next # save
current.next = prev_node # flip
prev_node = current # step
current = next_node # step
# --- 3) Walk both halves together and compare values. ---
front = head # start of the first half
back = prev_node # start of the reversed second half
while back is not None:
if front.val != back.val:
# One mismatch is enough: not a palindrome.
return False
front = front.next
back = back.next
# Every pair matched.
return True
What the code is doing, line by line:
- Phase 1:
slowmoves 1 node per round andfastmoves 2. Whenfastcan't continue,slowis at the start of the second half (for odd length it is the middle node, which is fine: it compares with itself). - Phase 2: reverse the second half in place with save / flip / step / step, starting at
slow. Afterwardsprev_nodeis the head of the reversed second half. - Phase 3:
frontstarts at the head andbackatprev_node. Whilebackstill has nodes, compare the values. if front.val != back.val: return False: one mismatch is enough to prove it is not a palindrome.- Otherwise step both pointers. If the loop ends without a mismatch, every pair matched, so return
True.
Dry run on LeetCode example head = [1,2,2,1] (expected True). Two nodes have value 2, so the node position is shown too:
| Phase | Variables | What happened |
|---|---|---|
| 1 find middle | slow = 1 (node #1), fast = 1 (node #1) | start |
| 1 find middle | slow = 2 (node #2), fast = 2 (node #3) | slow +1, fast +2 |
| 1 find middle | slow = 2 (node #3), fast = None | slow +1, fast +2 |
| 1 find middle | second half starts at slow = 2 (node #3) | |
| 2 reverse | flipped 2, next_node = 1 | reversed half: 2 |
| 2 reverse | flipped 1, next_node = None | reversed half: 1 → 2 |
| 2 reverse | first half (from head): 1 → 2 → 2 | |
| 3 compare | front = 1 (node #1), back = 1 (node #4) | equal |
| 3 compare | front = 2 (node #2), back = 2 (node #3) | equal |
| return | back = None | every pair matched → return True |
Edge cases to test:
- A single node → True.
- Even and odd lengths.
- Two nodes with different values → False.
Complexity: O(n) time, O(1) space.
Medium 143. Reorder List Google Meta Amazon Microsoft
Problem: Rearrange L0 → L1 → … → Ln into L0 → Ln → L1 → Ln−1 → … by changing links only.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,2,3,4][1,4,2,3]head = [1,2,3,4,5][1,5,2,4,3]Constraints: The number of nodes in the list is in the range [1, 5 * 10⁴]. · 1 ≤ Node.val ≤ 1000
Hint 1
The result interleaves the first half with the reversed second half.
Hint 2
Three steps: find the middle (fast/slow), reverse the second half, then weave the two halves together.
From brute force to the pattern: Copying the nodes into an array and re-linking with two indices is O(n) time but O(n) space. Middle + reverse + merge does it in place in O(n) time and O(1) space.
Spot it: You need the back half in reverse order → split at the middle, reverse the second half, then weave.
Approach:
- Fast/slow with
while fast.next and fast.next.nextputsslowat the end of the first half. - Cut after
slowand reverse the second half. - Weave:
front.next = back,back.next = front_next, then advance both. - Stop when the (shorter or equal) second half runs out.
class Solution:
def reorderList(self, head: Optional[ListNode]) -> None:
# 0 or 1 node: nothing to reorder.
if head is None or head.next is None:
return
# --- 1) Move slow to the END of the first half. ---
slow = head
fast = head
while fast.next is not None and fast.next.next is not None:
slow = slow.next
fast = fast.next.next
# --- 2) Cut the list in two and reverse the second half. ---
prev_node = None
current = slow.next # the second half starts after slow
slow.next = None # cut: the first half now ends at slow
while current is not None:
next_node = current.next # save
current.next = prev_node # flip
prev_node = current # step
current = next_node # step
# --- 3) Weave the halves: front1, back1, front2, back2, ... ---
front = head
back = prev_node
while back is not None:
# Remember where each half continues before we change links.
front_next = front.next
back_next = back.next
# front -> back -> (old front.next)
front.next = back
back.next = front_next
# Move both pointers to their saved next nodes.
front = front_next
back = back_next
What the code is doing, line by line:
if head is None or head.next is None: return: 0 or 1 nodes, nothing to reorder.- Phase 1: with the condition
fast.next is not None and fast.next.next is not None,slowstops at the end of the first half (the first half gets the extra node when the length is odd). - Phase 2:
current = slow.nextis the start of the second half;slow.next = Nonecuts the list into two separate lists. Then save / flip / step / step reverses the second half, andprev_nodeis its new head. - Phase 3 (weave):
frontwalks the first half andbackthe reversed second half. Before changing links, savefront_nextandback_next. front.next = backandback.next = front_nextput one back-node between two front-nodes.- Move
frontandbackto their saved next nodes. The loop ends when the second half (never longer than the first) runs out.
Dry run on LeetCode example head = [1,2,3,4,5] (expected [1,5,2,4,3]):
| Phase | Variables | List state |
|---|---|---|
| 1 find middle | slow = 1, fast = 1 | start |
| 1 find middle | slow = 2, fast = 3 | slow +1, fast +2 |
| 1 find middle | slow = 3, fast = 5 | slow +1, fast +2 |
| 2 cut | slow = 3 | first half: 1 → 2 → 3 | second half: 4 → 5 |
| 2 reverse | flipped 4 | reversed second half: 4 |
| 2 reverse | flipped 5 | reversed second half: 5 → 4 |
| 3 weave | front = 1, back = 5 → next front = 2, next back = 4 | list: 1 → 5 → 2 → 3 |
| 3 weave | front = 2, back = 4 → next front = 3, next back = None | list: 1 → 5 → 2 → 4 → 3 |
Edge cases to test:
- One or two nodes → unchanged.
- Odd length: the middle node ends up last.
- Forgetting to cut the first half off (set its tail's
nexttoNone) creates a cycle.
Complexity: O(n) time, O(1) space.
Medium 61. Rotate List Google Meta Amazon Microsoft
Problem: Rotate the list to the right by k places: the last k nodes move to the front.
head = [1,2,3,4,5], k = 2[4,5,1,2,3]head = [0,1,2], k = 4[2,0,1]Constraints: The number of nodes in the list is in the range [0, 500]. · -100 ≤ Node.val ≤ 100 · 0 ≤ k ≤ 2 * 10⁹
Hint 1
Rotating by the list's length changes nothing, so use k % length.
Hint 2
Make the list circular, then break it length − k nodes after the old head.
From brute force to the pattern: Rotating one step at a time is O(n · k). Measuring the length once, closing the ring and cutting in the right place is O(n).
Spot it: Re-linking pointers in place, like reversal. The trick is to close the list into a ring and cut it at the right spot.
Approach:
- Measure the
lengthand find the tail. shift = k % length; if it's 0, returnheadunchanged.- Link
tail.next = headto make a ring. - Walk
length − shift − 1steps fromheadto the new tail; the new head isnew_tail.next; then cut the ring.
class Solution:
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 0 or 1 node: rotating changes nothing.
if head is None or head.next is None:
return head
# --- 1) Measure the length and find the tail. ---
length = 1
tail = head
while tail.next is not None:
tail = tail.next
length = length + 1
# --- 2) Rotating by 'length' places changes nothing, so drop full turns. ---
# shift = how many places we really need to rotate.
shift = k % length
if shift == 0:
return head
# --- 3) Close the list into a ring. ---
tail.next = head
# --- 4) The new tail is length - shift - 1 steps from the head. ---
new_tail = head
for step in range(length - shift - 1):
new_tail = new_tail.next
# The node after the new tail is the new head.
new_head = new_tail.next
# Cut the ring open.
new_tail.next = None
return new_head
What the code is doing, line by line:
if head is None or head.next is None: return head: 0 or 1 nodes can't change by rotating.length = 1,tail = head, then walktailto the last node, adding 1 tolengthper step.shift = k % length: rotating bylengthplaces gives the same list back, so only the remainder matters. Ifshiftis 0, returnheadunchanged.tail.next = headcloses the list into a ring.- The last
shiftnodes must move to the front, so the new tail is the node at positionlength − shift, which islength − shift − 1steps from the head. Thefor step in range(length - shift - 1)loop walks there. new_head = new_tail.next, thennew_tail.next = Nonecuts the ring open. Returnnew_head.
Dry run on LeetCode example head = [1,2,3,4,5], k = 2 (expected [4,5,1,2,3]):
| Phase | Variables | What happened |
|---|---|---|
| measure | length = 1, tail = 1 | start at head |
| measure | length = 2, tail = 2 | walk one node |
| measure | length = 3, tail = 3 | walk one node |
| measure | length = 4, tail = 4 | walk one node |
| measure | length = 5, tail = 5 | walk one node |
| shift | shift = k % length = 2 % 5 = 2 | shift is not 0, keep going |
| ring | tail.next = head | 5 now points to 1 (the list is a circle) |
| walk | new_tail = 1 | need length - shift - 1 = 2 steps |
| walk | new_tail = 2 | step 1 |
| walk | new_tail = 3 | step 2 |
| cut | new_head = 4, 3.next = None | result: 4 → 5 → 1 → 2 → 3 |
Edge cases to test:
k = 0orka multiple of the length → unchanged.- Empty list or a single node.
- Very large
k(e.g. 2·10⁹): you must take% length.
Complexity: O(n) time, O(1) space.
Medium 445. Add Two Numbers II Google
Problem: Two non-negative numbers are stored as lists with the most-significant digit first. Return their sum as a list in the same order.
l1 = [7,2,4,3], l2 = [5,6,4][7,8,0,7]l1 = [2,4,3], l2 = [5,6,4][8,0,7]Constraints: The number of nodes in each linked list is in the range [1, 100]. · 0 ≤ Node.val ≤ 9 · It is guaranteed that the list represents a number that does not have leading zeros.
Hint 1
The most significant digit comes first, but addition starts from the least significant one.
Hint 2
Reverse both lists (or push the digits onto stacks), add with a carry, and build the result from the front.
From brute force to the pattern: Converting to integers works in Python but defeats the purpose and fails in languages with fixed-size integers. Reversing the lists, adding column by column with a carry, and reversing back is O(m + n).
Spot it: Addition starts from the least-significant digit, which is at the end. Reverse both lists in place first.
Key insight: Build the answer by prepending each new digit node. It comes out most-significant first, so no final reversal is needed.
Approach:
- Reverse both input lists (template A).
- Add digit by digit with a carry:
digit = total % 10andcarry = total // 10. - Prepend a node for
digitto the result. - Continue while either list or the carry remains.
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
# --- 1) Reverse both lists so the ones digit comes first. ---
first = self.reverse_list(l1)
second = self.reverse_list(l2)
# --- 2) Add digit by digit, like on paper, keeping a carry. ---
carry = 0
result = None # the answer list, built from the back
while first is not None or second is not None or carry != 0:
total = carry
# Add the digit from the first number, if it still has digits.
if first is not None:
total = total + first.val
first = first.next
# Add the digit from the second number, if it still has digits.
if second is not None:
total = total + second.val
second = second.next
digit = total % 10 # the digit we write down
carry = total // 10 # the 1 we carry to the next column
# PREPEND the new digit: put it in front of the answer so far.
# The answer then comes out most-significant digit first.
new_node = ListNode(digit)
new_node.next = result
result = new_node
return result
def reverse_list(self, head):
# Template A: save, flip, step, step.
prev_node = None
current = head
while current is not None:
next_node = current.next
current.next = prev_node
prev_node = current
current = next_node
return prev_node
What the code is doing, line by line:
first = self.reverse_list(l1)andsecond = self.reverse_list(l2): the helper method at the bottom is template A. After reversing, each list starts with its ones digit, which is where adding starts.carry = 0andresult = None: no carry yet and an empty answer list.while first is not None or second is not None or carry != 0:keep going while either number still has digits or a carry is left over (e.g. 99 + 1 needs an extra digit).total = carry, then eachifblock adds that number's digit (only if it still has one) and moves its pointer forward. This replaces the compacta.val if a else 0trick.digit = total % 10is the digit we write down;carry = total // 10is the 1 (or 0) carried to the next column.new_node.next = resultthenresult = new_nodeprepends the digit. We produce digits ones-first, and prepending each one puts the answer most-significant-first, as required.- Return
result.
Dry run on LeetCode example l1 = [7,2,4,3], l2 = [5,6,4] (7243 + 564 = 7807, expected [7,8,0,7]):
| Step | digit from first | digit from second | carry in | total | digit / carry out | result (after prepend) |
|---|---|---|---|---|---|---|
| reverse | 3 → 4 → 2 → 7 | 4 → 6 → 5 | — | — | — | None |
| column 1 | 3 | 4 | 0 | 7 | 7 / 0 | 7 |
| column 2 | 4 | 6 | 0 | 10 | 0 / 1 | 0 → 7 |
| column 3 | 2 | 5 | 1 | 8 | 8 / 0 | 8 → 0 → 7 |
| column 4 | 7 | — | 0 | 7 | 7 / 0 | 7 → 8 → 0 → 7 |
Edge cases to test:
- Different lengths, e.g.
[7, 2, 4, 3]+[5, 6, 4]. - A final carry adds a new leading digit, e.g.
[5]+[5]→[1, 0]. - One number is 0.
Complexity: O(m + n) time, O(1) extra space besides the output.
Medium 2074. Reverse Nodes in Even Length Groups Google
Problem: Split the list into consecutive groups of sizes 1, 2, 3, … (the last group may be shorter). Reverse every group whose actual length is even.
head = [5,2,6,3,9,1,7,3,8,4][5,6,2,3,9,1,4,8,3,7]head = [1,1,0,6][1,0,1,6]Constraints: The number of nodes in the list is in the range [1, 10⁵]. · 0 ≤ Node.val ≤ 10⁵
Hint 1
Groups have lengths 1, 2, 3, … except the last group, which may be shorter. Measure each group's real length first.
Hint 2
Reverse a group only if its actual length is even, then reconnect it like in Reverse Nodes in k-Group.
From brute force to the pattern: Copying values into an array, reversing even-length chunks and writing them back uses O(n) space. Walking group by group and reversing in place is O(n) time and O(1) space.
Spot it: Reversing chunks in place → k-group reversal where k grows each time and a parity check decides whether to reverse.
Approach:
group_prev = head(group 1 has size 1 and is never reversed);group_size = 2.- Count up to
group_sizenodes aftergroup_prevto get the group's real length. - If it's even, reverse those nodes with
prev_nodestarting atgroup_next(the node after the group), setgroup_prev.next = prev_node, and movegroup_prevtogroup_first(the old first node). - If it's odd, move
group_prevtogroup_last(the group's last node). group_size = group_size + 1; repeat until no nodes remain.
class Solution:
def reverseEvenLengthGroups(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Group 1 is just the head (size 1, odd), so it is never reversed.
# group_prev = the node just BEFORE the current group.
group_prev = head
group_size = 2 # the size the next group SHOULD have
while group_prev.next is not None:
# --- 1) Count the real length of this group (the last may be shorter). ---
group_last = group_prev
count = 0
while count < group_size and group_last.next is not None:
group_last = group_last.next
count = count + 1
if count % 2 == 0:
# --- 2a) Even length: reverse the group (like k-group). ---
group_first = group_prev.next
group_next = group_last.next
# Start prev_node at group_next so the old first node
# ends up pointing at the rest of the list.
prev_node = group_next
current = group_first
while current is not group_next:
next_node = current.next # save
current.next = prev_node # flip
prev_node = current # step
current = next_node # step
# Stitch the front: prev_node is the old last node = new first node.
group_prev.next = prev_node
# The old first node is now the group's tail.
group_prev = group_first
else:
# --- 2b) Odd length: leave it alone, just jump over it. ---
group_prev = group_last
# The next group should be one node longer.
group_size = group_size + 1
return head
What the code is doing, line by line:
group_prev = head: group 1 is just the head. Its size is 1 (odd), so it is never reversed, and the head never changes. That's why no dummy is needed.group_size = 2: the size the next group should have. It grows by 1 after every group.while group_prev.next is not None:continue while there are nodes after the last finished group.- Count the real length: walk
group_lastforward up togroup_sizenodes, counting them incount. The last group may be shorter, which is why we usecount(notgroup_size) to decide. - Even
count: namegroup_firstandgroup_next, then reverse with save / flip / step / step, startingprev_nodeatgroup_next(same trick as problem 25) so the old first node points at the rest of the list. group_prev.next = prev_nodestitches the front (prev_nodeis the old last node = new first).group_prev = group_first, because the old first node is now the group's tail.- Odd
count: leave the group alone and jump over it:group_prev = group_last. group_size = group_size + 1, then do the next group. Returnheadat the end.
Dry run on LeetCode example head = [5,2,6,3,9,1,7,3,8,4] (expected [5,6,2,3,9,1,4,8,3,7]):
| Group | group_size | count (real) | nodes in group | Action | list after this group |
|---|---|---|---|---|---|
| group 1 | 1 | 1 | 5 | odd (head, never reversed) | 5 → 2 → 6 → 3 → 9 → 1 → 7 → 3 → 8 → 4 |
| group 2 | 2 | 2 | 2, 6 | even → reversed | 5 → 6 → 2 → 3 → 9 → 1 → 7 → 3 → 8 → 4 |
| group 3 | 3 | 3 | 3, 9, 1 | odd → skipped | 5 → 6 → 2 → 3 → 9 → 1 → 7 → 3 → 8 → 4 |
| group 4 | 4 | 4 | 7, 3, 8, 4 | even → reversed | 5 → 6 → 2 → 3 → 9 → 1 → 4 → 8 → 3 → 7 |
Edge cases to test:
- The last group is shorter and its length is even → reverse it.
- The last group is shorter and odd → leave it.
- A list of 1 or 2 nodes.
Complexity: O(n) time, O(1) space.
Easy 21. Merge Two Sorted Lists Start here Google Meta Amazon Microsoft
Problem: Merge two sorted linked lists into one sorted list by splicing their nodes together. Return the new head.
Solve it on LeetCode ↗ · Video walkthrough ↗
list1 = [1,2,4], list2 = [1,3,4][1,1,2,3,4,4]list1 = [], list2 = [][]Constraints: The number of nodes in both lists is in the range [0, 50]. · -100 ≤ Node.val ≤ 100 · Both list1 and list2 are sorted in non-decreasing order.
Hint 1
Compare the two front nodes. Which goes first?
Hint 2
Attach the smaller front node to a tail pointer behind a dummy node; at the end, attach whatever is left.
From brute force to the pattern: Copying all values into an array, sorting and rebuilding is O((m + n) log(m + n)) and allocates new nodes. Merging by re-linking the existing nodes is O(m + n) time and O(1) extra space.
Spot it: Two sorted sequences → compare the two fronts and take the smaller (the merge step of merge sort). With linked lists we re-point next pointers instead of copying values.
Approach:
- Create a
dummynode and atailpointer. - While both lists have nodes: attach the smaller front node to
tail, advance that list, and movetail. - One list may have nodes left. They are already sorted, so attach the rest in one step.
- Return
dummy.next.
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode() # placeholder in front of the merged list
tail = dummy
while list1 is not None and list2 is not None:
if list1.val <= list2.val:
tail.next = list1 # reuse list1's node
list1 = list1.next
else:
tail.next = list2
list2 = list2.next
tail = tail.next
# at most one list still has nodes, and they are already sorted
if list1 is not None:
tail.next = list1
else:
tail.next = list2
return dummy.next
What the code is doing, line by line:
dummy,tail:tailis always the last node of the merged list so far.while list1 is not None and list2 is not None:compare only while both have a front node.if list1.val <= list2.val:take the smaller front.<=keeps equal values in their original order (a stable merge).tail.next = list1re-uses the existing node;list1 = list1.nextmoves that list forward;tail = tail.nextmoves to the new end.- After the loop, attach whichever list is left. It's already sorted and bigger than everything placed so far.
Dry run on the example:
list1 = [1, 2, 4], list2 = [1, 3, 4] (LeetCode Example 1). Returns [1, 1, 2, 3, 4, 4].
| list1 front | list2 front | take | merged so far |
|---|---|---|---|
| 1 | 1 | list1's 1 (tie → list1) | 1 |
| 2 | 1 | list2's 1 | 1 1 |
| 2 | 3 | list1's 2 | 1 1 2 |
| 4 | 3 | list2's 3 | 1 1 2 3 |
| 4 | 4 | list1's 4 (tie) | 1 1 2 3 4 |
| None | 4 | list1 is empty → attach the rest of list2 | 1 1 2 3 4 4 |
Edge cases to test:
- One or both lists empty.
- Equal values in both lists.
- Every value of one list smaller than every value of the other.
Complexity: O(m + n) time, O(1) extra space (nodes are re-linked, not copied).
Easy 83. Remove Duplicates from Sorted List Google Meta Amazon Microsoft
Problem: The linked list is sorted. Delete duplicates so each value appears only once, and return the head.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,1,2][1,2]head = [1,1,2,3,3][1,2,3]Constraints: The number of nodes in the list is in the range [0, 300]. · -100 ≤ Node.val ≤ 100 · The list is guaranteed to be sorted in ascending order.
Hint 1
The list is sorted, so duplicates are next to each other.
Hint 2
If cur.next has the same value as cur, skip it by re-pointing cur.next; otherwise move on.
From brute force to the pattern: A set of seen values plus rebuilding the list uses O(n) space. Because the list is sorted, comparing each node with its neighbour removes duplicates in place: O(n) time, O(1) space.
Spot it: Sorted means equal values sit next to each other. Stay on a node and unlink its next for as long as it has the same value. That's in-place pointer surgery with one pointer.
Approach:
cur = head.- While
curandcur.nextexist: ifcur.next.val == cur.val, unlinkcur.next(cur.next = cur.next.next) and stay oncur. - Otherwise move
curforward. - Return
head(the first node is never removed).
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
cur = head
while cur is not None and cur.next is not None:
if cur.next.val == cur.val:
# same value again: unlink the next node, but stay here
# (the new next might be another copy)
cur.next = cur.next.next
else:
# a new value: move on to it
cur = cur.next
return head
What the code is doing, line by line:
cur = head: the node whose duplicates we are removing.while cur is not None and cur.next is not None:we look one node ahead, so both must exist.if cur.next.val == cur.val: cur.next = cur.next.next: drop the copy. We do not movecur, because a run like1 → 1 → 1needs several removals.else: cur = cur.next: the next value is different, so it becomes the new current node.return head: the head is the first of its value, so it always stays.
Dry run on the example:
head = [1, 1, 2, 3, 3] (LeetCode Example 2). Returns [1, 2, 3].
| cur | cur.next | equal? | action | list now |
|---|---|---|---|---|
| 1 | 1 | yes | unlink | 1 → 2 → 3 → 3 |
| 1 | 2 | no | move on | 1 → 2 → 3 → 3 |
| 2 | 3 | no | move on | 1 → 2 → 3 → 3 |
| 3 | 3 | yes | unlink | 1 → 2 → 3 |
| 3 | None | — | stop | 1 → 2 → 3 |
Edge cases to test:
- An empty list.
- All values equal → one node remains.
- Duplicates at the end of the list.
Complexity: O(n) time, O(1) space.
Medium 2. Add Two Numbers Google Meta Amazon Cisco Microsoft
Problem: Two non-empty linked lists store non-negative integers with their digits in reverse order (2 → 4 → 3 is 342). Return their sum as a linked list in the same reverse format.
Solve it on LeetCode ↗ · Video walkthrough ↗
l1 = [2,4,3], l2 = [5,6,4][7,0,8]l1 = [0], l2 = [0][0]Constraints: The number of nodes in each linked list is in the range [1, 100]. · 0 ≤ Node.val ≤ 9 · It is guaranteed that the list represents a number that does not have leading zeros.
Hint 1
The digits are stored least significant first, which is exactly the order you add them in.
Hint 2
Walk both lists at the same time, adding digit + digit + carry; keep going while either list or the carry remains.
From brute force to the pattern: Converting both lists to integers and back works in Python but would overflow in most languages and misses the point. Column addition with a carry, one node at a time, is O(max(m, n)).
Spot it: Reverse order means the head is the ones digit, exactly where column addition starts. So walk both lists together, add digit + digit + carry, and append one new node per column.
Approach:
- Make a
dummynode and atailpointer to append to;carry = 0. - While either list has nodes left or
carryis not zero: add the two current digits (0 for a list that has ended) andcarry. - The new digit is
total % 10; the new carry istotal // 10. Append the digit and movetail. - Advance whichever input lists still have nodes.
- Return
dummy.next.
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode() # the answer is built after this placeholder
tail = dummy
carry = 0
# a final carry (e.g. 5 + 5 = 10) still needs its own node
while l1 is not None or l2 is not None or carry != 0:
total = carry
if l1 is not None:
total = total + l1.val
l1 = l1.next
if l2 is not None:
total = total + l2.val
l2 = l2.next
# write the ones digit, carry the tens digit to the next column
tail.next = ListNode(total % 10)
tail = tail.next
carry = total // 10
return dummy.next
What the code is doing, line by line:
dummy,tail: appending after a placeholder means the first digit needs no special case.while l1 is not None or l2 is not None or carry != 0:keep going while any digit or carry remains. The lists can have different lengths.total = carry, then addl1.valandl2.valonly if that list still has a node, stepping it forward as we use it.ListNode(total % 10): the digit for this column (e.g. 10 → 0).carry = total // 10is 0 or 1 for the next column.return dummy.next: skip the placeholder.
Dry run on the example:
l1 = [2, 4, 3] (342), l2 = [5, 6, 4] (465) (LeetCode Example 1). Returns [7, 0, 8] (807).
| column | l1 digit | l2 digit | carry in | total | new node | carry out |
|---|---|---|---|---|---|---|
| ones | 2 | 5 | 0 | 7 | 7 | 0 |
| tens | 4 | 6 | 0 | 10 | 0 | 1 |
| hundreds | 3 | 4 | 1 | 8 | 8 | 0 |
Edge cases to test:
- Different lengths.
- A carry at the very end, e.g.
[9, 9]+[1]→[0, 0, 1]. - Both numbers are 0.
Complexity: O(max(m, n)) time, O(max(m, n)) for the output list.
Medium 82. Remove Duplicates from Sorted List II Google Meta Amazon
Problem: The linked list is sorted. Delete every node whose value appears more than once, leaving only values that were distinct. Return the head.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,2,3,3,4,4,5][1,2,5]head = [1,1,1,2,3][2,3]Constraints: The number of nodes in the list is in the range [0, 300]. · -100 ≤ Node.val ≤ 100 · The list is guaranteed to be sorted in ascending order.
Hint 1
Every copy of a repeated value must go, including the first one, and the head might be one of them.
Hint 2
Use a dummy node and a prev pointer on the last node you're sure to keep; skip whole runs of equal values.
From brute force to the pattern: Counting each value's occurrences and rebuilding the list uses O(n) space. Since the list is sorted, duplicates form runs that can be skipped in place: O(n) time, O(1) space.
Spot it: Unlike LeetCode 83, the first copy must go too, and that may be the head. A dummy node plus a prev pointer that always sits on the last node we are sure to keep makes this a clean in-place splice.
Approach:
dummy → head;prev = dummy(the last kept node),cur = head.- If
curstarts a run of equal values (cur.nexthas the same value), walkcurto the last node of the run, then splice the whole run out:prev.next = cur.next. - Otherwise
curis unique: keep it by movingprevonto it. - Move
curforward and repeat. Returndummy.next.
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head) # the head itself may be deleted
prev = dummy # last node we are sure to keep
cur = head
while cur is not None:
if cur.next is not None and cur.next.val == cur.val:
# cur starts a run of duplicates: walk to the run's last node
while cur.next is not None and cur.next.val == cur.val:
cur = cur.next
# cut the whole run out; prev stays where it is
prev.next = cur.next
else:
# cur is unique: it stays, so prev moves onto it
prev = cur
cur = cur.next
return dummy.next
What the code is doing, line by line:
dummy = ListNode(0, head): lets us delete the head with the same code as any other node.prev: the last node that is definitely in the answer. We only ever splice after it.if cur.next is not None and cur.next.val == cur.val:curis the first node of a duplicate run.- Inner
while: movecurto the last copy of that value. prev.next = cur.next: one assignment removes the entire run.prevdoes not move: the next node might start another run.else: prev = cur: a value with no copies is kept.cur = cur.next: continue after the run (or after the kept node).
Dry run on the example:
head = [1, 2, 3, 3, 4, 4, 5] (LeetCode Example 1). Returns [1, 2, 5].
| cur | run of duplicates? | action | prev after | list now |
|---|---|---|---|---|
| 1 | no | keep 1 | 1 | 1 2 3 3 4 4 5 |
| 2 | no | keep 2 | 2 | 1 2 3 3 4 4 5 |
| 3 | yes (3, 3) | 2.next = 4, the 3s are gone | 2 | 1 2 4 4 5 |
| 4 | yes (4, 4) | 2.next = 5, the 4s are gone | 2 | 1 2 5 |
| 5 | no | keep 5 | 5 | 1 2 5 |
Edge cases to test:
- All values duplicated → empty list.
- The head is part of a duplicate run.
- No duplicates → unchanged.
Complexity: O(n) time, O(1) space.
Medium 138. Copy List with Random Pointer Google Meta Nvidia Amazon Microsoft
Problem: Each node has a next pointer and a random pointer that may point to any node or None. Return a deep copy: brand-new nodes whose pointers mirror the original structure.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [[7,null],[13,0],[11,4],[10,2],[1,0]][[7,null],[13,0],[11,4],[10,2],[1,0]]head = [[1,1],[2,1]][[1,1],[2,1]]Constraints: 0 ≤ n ≤ 1000 · -10⁴ ≤ Node.val ≤ 10⁴ · Node.random is null or is pointing to some node in the linked list.
Hint 1
random can point to a node whose copy doesn't exist yet.
Hint 2
Create every copy first (map old node → new node), then wire up next and random in a second pass.
From brute force to the pattern: Searching the list to find where each random points is O(n²). An old → new hash map makes every lookup O(1): two passes, O(n). (Weaving the copies between the originals gets the space down to O(1).)
Spot it: The trouble is random: it may point to a node whose copy doesn't exist yet. A hash map from old node → new node solves it. Create all copies first, then connect them.
Key insight: Two passes. Pass 1 creates a copy of every node. Pass 2 sets copy.next = old_to_new[old.next] and copy.random = old_to_new[old.random]. Mapping None → None avoids special cases. (An O(1)-space variant weaves copies between the originals, but the map version is far easier to get right.)
Approach:
- Start the map with
{None: None}, so a missing pointer copies to a missing pointer. - Pass 1: walk the list and create
Node(old.val)for every node, storingold → copy. - Pass 2: walk again and set each copy's
nextandrandomby looking up the copies of the old targets. - Return the copy of
head.
class Solution:
def copyRandomList(self, head: "Optional[Node]") -> "Optional[Node]":
# old node -> its copy; None maps to None so empty pointers copy for free
old_to_new = {None: None}
# pass 1: make a copy of every node (pointers not set yet)
cur = head
while cur is not None:
old_to_new[cur] = Node(cur.val)
cur = cur.next
# pass 2: wire up next and random using the copies
cur = head
while cur is not None:
copy = old_to_new[cur]
copy.next = old_to_new[cur.next]
copy.random = old_to_new[cur.random]
cur = cur.next
return old_to_new[head]
What the code is doing, line by line:
old_to_new = {None: None}: nodes can be dictionary keys (by identity). SeedingNonemeansold_to_new[cur.random]works even whenrandomis empty.- Pass 1,
old_to_new[cur] = Node(cur.val): every copy exists before we connect anything, so pass 2 can point to any of them. - Pass 2,
copy.next = old_to_new[cur.next]: the copy's next is the copy of the old next, never the old node itself. copy.random = old_to_new[cur.random]: same idea for the random pointer.return old_to_new[head]: the copied head (orNonefor an empty list).
Dry run on the example:
head = [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]] (LeetCode Example 1; each pair is [value, index that random points to]). Name the originals A..E and the copies A'..E'.
| old node | val | old.next | old.random | copy gets next / random |
|---|---|---|---|---|
| A | 7 | B | None | A'.next = B', A'.random = None |
| B | 13 | C | A | B'.next = C', B'.random = A' |
| C | 11 | D | E | C'.next = D', C'.random = E' |
| D | 10 | E | C | D'.next = E', D'.random = C' |
| E | 1 | None | A | E'.next = None, E'.random = A' |
Pass 1 already created A'..E', so every lookup in pass 2 succeeds. The copy has the same shape, [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]], with no shared nodes.
Edge cases to test:
- An empty list.
randompointing to the node itself.randomisNoneeverywhere.
Complexity: O(n) time, O(n) space for the map.
Medium 2807. Insert Greatest Common Divisors in Linked List
Problem: Between every pair of adjacent nodes, insert a new node whose value is the greatest common divisor (GCD) of the two values. Return the head.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [18,6,10,3][18,6,6,2,10,1,3]head = [7][7]Constraints: The number of nodes in the list is in the range [1, 5000]. · 1 ≤ Node.val ≤ 1000
Hint 1
For every adjacent pair, a new node goes between them.
Hint 2
Create the node already pointing at the next original node, then link the current node to it and jump over it.
From brute force to the pattern: Collecting values into an array and building a new list works but allocates the whole list again. Inserting in place while walking once is O(n · log(max value)) for the GCDs.
Spot it: Pure in-place pointer surgery: for each pair (cur, cur.next), create a node, point it at cur.next, point cur at it, then jump over it to the old next node.
Approach:
cur = head.- While
cur.nextexists: computeg = gcd(cur.val, cur.next.val). - Create
ListNode(g, cur.next): it already points at the old next node. Then setcur.nextto the new node. - Jump
curtwo steps (over the new node) to the old next node, and repeat.
class Solution:
def insertGreatestCommonDivisors(self, head: Optional[ListNode]) -> Optional[ListNode]:
def gcd(a, b):
# Euclid: gcd(a, b) = gcd(b, a % b), and gcd(a, 0) = a
while b != 0:
a, b = b, a % b
return a
cur = head
while cur is not None and cur.next is not None:
after = cur.next
# the new node points at the old next node ...
middle = ListNode(gcd(cur.val, after.val), after)
# ... and cur points at the new node
cur.next = middle
# skip over the node we just added
cur = after
return head
What the code is doing, line by line:
def gcd(a, b):Euclid's algorithm: replace(a, b)with(b, a % b)untilbis 0; thenais the GCD. (Python'smath.gcddoes the same.)while cur is not None and cur.next is not None:we need a pair, so the next node must exist.after = cur.next: remember the old neighbour before we change any pointer.middle = ListNode(gcd(...), after): build the new node already linked toafter.cur.next = middle: link it in. The order matters: if we changedcur.nextfirst without savingafter, we would lose the rest of the list.cur = after: move to the next original node, skipping the one we inserted.
Dry run on the example:
head = [18, 6, 10, 3] (LeetCode Example 1). Returns [18, 6, 6, 2, 10, 1, 3].
| cur | after | gcd | list now |
|---|---|---|---|
| 18 | 6 | 6 | 18 → 6 → 6 → 10 → 3 |
| 6 | 10 | 2 | 18 → 6 → 6 → 2 → 10 → 3 |
| 10 | 3 | 1 | 18 → 6 → 6 → 2 → 10 → 1 → 3 |
| 3 | None | — | stop |
Edge cases to test:
- A single node → nothing to insert.
- Equal neighbours: the GCD is the value itself.
- Coprime neighbours → 1.
Complexity: O(n · log(max value)) time (one GCD per pair), O(1) extra space besides the new nodes.
Medium 86. Partition List Google Meta Amazon Microsoft
Problem: Rearrange the list so all nodes with values less than x come before nodes ≥ x, preserving the original order within each group.
head = [1,4,3,2,5,2], x = 3[1,2,2,4,3,5]head = [2,1], x = 2[1,2]Constraints: The number of nodes in the list is in the range [0, 200]. · -100 ≤ Node.val ≤ 100 · -200 ≤ x ≤ 200
Hint 1
Think of building two separate lists while walking the original once.
Hint 2
Use two dummy heads, then connect the end of the first list to the start of the second.
From brute force to the pattern: Collecting values into two arrays and rebuilding new nodes is O(n) time but O(n) extra space. Re-linking the existing nodes into two chains needs O(1) space.
Spot it: Splitting one list into two order-preserving groups is in-place pointer surgery: walk once and append each node to the tail of one of two dummy-headed lists, then join them.
Why not another pattern? Copying values into arrays and rebuilding works but allocates new nodes. Swapping values in place would break the relative order.
Approach:
- Create two dummy heads:
smallandbig, with tails. - Walk the list; append each node to the
smalltail ifval < x, else to thebigtail. - End the
biglist withNone(its last node may still point somewhere), then linksmall_tail.next = big_head.next.
class Solution:
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
small = ListNode() # dummy head for values < x
big = ListNode() # dummy head for values >= x
small_tail = small
big_tail = big
cur = head
while cur is not None:
if cur.val < x:
small_tail.next = cur # append to the "small" list
small_tail = cur
else:
big_tail.next = cur # append to the "big" list
big_tail = cur
cur = cur.next
big_tail.next = None # cut off the old link to avoid a cycle
small_tail.next = big.next # small list, then big list
return small.next
What the code is doing, line by line:
- Two dummy heads mean the first node of either group needs no special case.
- Each node is re-linked onto one of the two tails in the order it's visited, which preserves relative order.
big_tail.next = None: the last big node might still point to a small node from the original list.small_tail.next = big.nextjoins the groups;small.nextis the new head.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All values less than
x(or all ≥x). - An empty list.
- Forgetting
big_tail.next = Nonecreates a cycle.
Complexity: O(n) time, O(1) extra space.
Medium 109. Convert Sorted List to Binary Search Tree Google Amazon
Problem: Convert a sorted singly linked list into a height-balanced binary search tree.
head = [-10,-3,0,5,9][0,-3,9,-10,null,5]head = [][]Constraints: The number of nodes in head is in the range [0, 2 * 10⁴]. · -10⁵ ≤ Node.val ≤ 10⁵
Hint 1
A BST's in-order traversal is sorted, just like the list.
Hint 2
Build the tree in in-order, consuming one list node each time you create a root.
From brute force to the pattern: Finding the middle node at every level with fast/slow pointers costs O(n) per level: O(n log n). Building in in-order uses each list node exactly once at the moment it's needed: O(n).
Spot it: The list is the BST's in-order sequence. Build the tree in in-order: recursively build the left half, take the current list node as the root, then build the right half, advancing one pointer through the list as you go.
Why not another pattern? Copying the list into an array and taking middles (LeetCode 108) is O(n) time but O(n) extra space. Finding the middle with fast/slow at every level is O(n log n).
Approach:
- Count the nodes
n. build(lo, hi)builds the tree for list positionslo..hi. Iflo > hi, returnNone.mid = (lo + hi) // 2; build the left subtree forlo..mid − 1first; this consumes exactly the nodes beforemid.- The current list node is now position
mid: make it the root, advance the list pointer, then build the right subtreemid + 1..hi.
class Solution:
def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
n = 0
cur = head
while cur is not None:
n = n + 1 # count the nodes
cur = cur.next
self.cur = head # the next list node to place (in-order)
def build(lo, hi):
if lo > hi:
return None
mid = (lo + hi) // 2
left = build(lo, mid - 1) # all nodes before mid are consumed here
root = TreeNode(self.cur.val) # the current list node is position mid
self.cur = self.cur.next
root.left = left
root.right = build(mid + 1, hi)
return root
return build(0, n - 1)
What the code is doing, line by line:
- Counting first lets
buildsplit ranges by index, exactly like the array version. self.curwalks the list in order; an in-order construction visits positions in increasing order too, so they stay in sync.- The left subtree is built before the root is created, so by then
self.curpoints at positionmid. - Picking the middle of each range keeps the tree balanced.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- An empty list →
None. - One or two nodes.
- Negative values.
Complexity: O(n) time, O(log n) recursion depth.
Medium 147. Insertion Sort List Google Amazon
Problem: Sort a singly linked list using insertion sort and return its head.
head = [4,2,1,3][1,2,3,4]head = [-1,5,3,4,0][-1,0,3,4,5]Constraints: The number of nodes in the list is in the range [1, 5000]. · -5000 ≤ Node.val ≤ 5000
Hint 1
Keep a separate sorted list and insert nodes into it one by one.
Hint 2
A dummy head makes inserting at the front the same as anywhere else.
From brute force to the pattern: Copying values into an array, sorting and rebuilding uses O(n) space. Splicing nodes keeps it in place.
Spot it: Insertion sort on a linked list is pure pointer surgery: detach each node and splice it into its place in a growing sorted list behind a dummy head (no shifting of elements needed).
Why not another pattern? Merge sort (LeetCode 148) is O(n log n) and faster; this problem asks specifically for insertion sort (O(n²) worst case).
Approach:
dummystarts an empty sorted list.- Take nodes from the input one at a time (remember
nextbefore re-linking). - Walk
prevfromdummyuntilprev.nextisNoneor larger than the node; splice the node afterprev.
class Solution:
def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode() # head of the sorted part
cur = head
while cur is not None:
nxt = cur.next # remember the rest of the unsorted list
prev = dummy
while prev.next is not None and prev.next.val < cur.val:
prev = prev.next # find where cur belongs
cur.next = prev.next # splice cur in after prev
prev.next = cur
cur = nxt
return dummy.next
What the code is doing, line by line:
dummy.nextis the sorted list built so far (initially empty).nxt = cur.next: save it beforecuris re-linked into the sorted list.- The inner loop stops at the last node smaller than
cur. - Two pointer assignments insert
curthere.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already sorted (each insertion walks to the end; you can optimise by starting from the last inserted node).
- Reverse sorted.
- Duplicates.
Complexity: O(n²) time in the worst case, O(1) extra space.
Medium 237. Delete Node in a Linked List Google Amazon Microsoft
Problem: You're given only the node to delete (not the head). It's not the last node. Delete it so the list's values no longer contain it.
head = [4,5,1,9], node = 5[4,1,9]head = [4,5,1,9], node = 1[4,5,9]Constraints: The number of the nodes in the given list is in the range [2, 1000]. · -1000 ≤ Node.val ≤ 1000 · The value of each node in the list is unique. · The node to be deleted is in the list and is not a tail node.
Hint 1
You can't reach the node before node. Which node can you remove easily?
Hint 2
Copy the next node's value into node, then delete the next node.
From brute force to the pattern: Normally you'd find the previous node from the head (O(n)), but there's no head. The copy-and-skip trick is O(1).
Spot it: A linked-list rewiring puzzle: without the previous node you can't unlink node itself, but you can copy the next node's value into it and unlink the next node.
Why not another pattern? Walking from the head to find the previous node is impossible here: you aren't given the head.
Approach:
- Copy
node.next.valintonode.val. - Skip over the next node:
node.next = node.next.next.
class Solution:
def deleteNode(self, node):
node.val = node.next.val # take over the next node's value
node.next = node.next.next # and remove the next node
What the code is doing, line by line:
- We can't change the previous node's pointer, so we make
nodelook like its successor. - Then the successor (now a duplicate) is unlinked.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Deleting the second-to-last node.
- Equal values in the list.
- The problem guarantees
nodeisn't the tail.
Complexity: O(1) time and space.
Medium 328. Odd Even Linked List Google Meta Amazon Apple
Problem: Group all nodes at odd positions first, then those at even positions (1-indexed), keeping the order inside each group, in O(1) space.
head = [1,2,3,4,5][1,3,5,2,4]head = [2,1,3,5,6,4,7][2,3,6,7,1,5,4]Constraints: The number of nodes in the linked list is in the range [0, 10⁴]. · -10⁶ ≤ Node.val ≤ 10⁶
Hint 1
Build two chains at the same time: positions 1, 3, 5, … and 2, 4, 6, …
Hint 2
At the end, connect the last odd node to the first even node.
From brute force to the pattern: Collecting nodes into two arrays and re-linking uses O(n) space; weaving the chains in place uses O(1).
Spot it: Splitting a list into two interleaved chains and re-joining them is in-place pointer surgery with two moving pointers.
Why not another pattern? Copying values into arrays and rebuilding uses O(n) extra space, which the problem forbids.
Approach:
odd = head,even = head.next, remembereven_head.- While
evenandeven.nextexist:odd.next = even.next, advanceodd;even.next = odd.next, advanceeven. - Attach the even chain:
odd.next = even_head.
class Solution:
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None
odd = head
even = head.next
even_head = even # remember where the even chain starts
while even is not None and even.next is not None:
odd.next = even.next # next odd node
odd = odd.next
even.next = odd.next # next even node
even = even.next
odd.next = even_head # odds first, then evens
return head
What the code is doing, line by line:
oddandevenare the tails of the two chains being built.- Each round links one odd node and one even node, skipping over the other chain's node.
- The loop condition guarantees
even.next(the next odd node) exists. - Finally the odd chain's tail points to the first even node.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- An empty list or a single node.
- Two nodes → unchanged.
- Odd and even lengths.
Complexity: O(n) time, O(1) space.
Medium 707. Design Linked List Google
Problem: Implement a singly linked list with get(index), addAtHead, addAtTail, addAtIndex(index, val) and deleteAtIndex(index).
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]][null, null, null, null, 2, null, 3]Constraints: 0 ≤ index, val ≤ 1000 · Please do not use the built-in LinkedList library. · At most 2000 calls will be made to get, addAtHead, addAtTail, addAtIndex and deleteAtIndex.
Hint 1
Most operations need the node before the target position.
Hint 2
A dummy head and a size counter remove almost all special cases.
From brute force to the pattern: Without a sentinel, inserting or deleting at index 0 needs separate code paths; the dummy node makes every position uniform.
Spot it: Every operation is "walk to the node before position index, then re-link". A dummy (sentinel) head makes index 0 behave like any other position, and a size counter answers bounds checks in O(1).
Why not another pattern? A Python list would make this trivial, but the exercise is about the pointer operations themselves.
Approach:
- Keep
self.dummy(sentinel before the real head) andself.size. - A helper
_before(index)walksindexsteps from the dummy: it returns the node just before positionindex. addAtIndex: rejectindex > size; splice a new node after_before(index). Head and tail additions areaddAtIndex(0, …)andaddAtIndex(size, …).getanddeleteAtIndex: rejectindex ≥ size; use_before(index)to read or unlink.
class MyLinkedList:
class Node:
def __init__(self, val, nxt=None):
self.val = val
self.next = nxt
def __init__(self):
self.dummy = self.Node(0) # sentinel in front of the real head
self.size = 0
def _before(self, index):
node = self.dummy
for _ in range(index):
node = node.next # walk to the node just before position index
return node
def get(self, index: int) -> int:
if index < 0 or index >= self.size:
return -1
return self._before(index).next.val
def addAtHead(self, val: int) -> None:
self.addAtIndex(0, val)
def addAtTail(self, val: int) -> None:
self.addAtIndex(self.size, val)
def addAtIndex(self, index: int, val: int) -> None:
if index < 0 or index > self.size:
return # invalid position: do nothing
prev = self._before(index)
prev.next = self.Node(val, prev.next) # splice the new node in
self.size = self.size + 1
def deleteAtIndex(self, index: int) -> None:
if index < 0 or index >= self.size:
return
prev = self._before(index)
prev.next = prev.next.next # unlink the node at index
self.size = self.size - 1
What the code is doing, line by line:
self.dummysits before position 0, so inserting at the head is the same code as inserting anywhere._before(index)takesindexsteps from the dummy, landing on the predecessor of positionindex.addAtIndexallowsindex == size(append) but not beyond it.getanddeleteAtIndexrequire an existing position:index < size.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
addAtIndex(size, val)appends.addAtIndex(size + 1, val)does nothing.- Deleting the only node.
Complexity: O(index) per operation (O(n) worst case), O(1) for addAtHead; O(n) space.
Medium 2058. Find the Minimum and Maximum Number of Nodes Between Critical Points Google
Problem: A critical point is a local maximum or minimum (strictly greater or smaller than both neighbours). Return [min distance, max distance] between any two critical points, or [-1, -1] if there are fewer than two.
head = [3,1][-1,-1]head = [5,3,1,2,5,1,2][1,3]Constraints: The number of nodes in the list is in the range [2, 10⁵]. · 1 ≤ Node.val ≤ 10⁵
Hint 1
You need each node together with its two neighbours.
Hint 2
The smallest gap is between consecutive critical points; the largest is between the first and the last.
From brute force to the pattern: Collecting every critical index and comparing all pairs is O(k²). Only consecutive gaps and the first/last pair matter, so one pass suffices.
Spot it: A single walk looking at each node with its neighbours (prev, cur, cur.next): linked-list traversal with a few remembered indices.
Why not another pattern? Collecting all critical indices in a list first works too (O(k) extra space); you only need the first, previous and latest ones.
Approach:
- Walk with
prevandcur, keeping the positioniofcur. - If
curis a local max or min, it's critical: compare with the previous critical index for the minimum gap, remember the first one for the maximum. - At the end, the maximum distance is
last − first; return[-1, -1]if fewer than two critical points.
class Solution:
def nodesBetweenCriticalPoints(self, head: Optional[ListNode]) -> List[int]:
first = -1 # index of the first critical point
last = -1 # index of the latest critical point
min_gap = float("inf")
prev = head
cur = head.next
i = 1
while cur is not None and cur.next is not None:
nxt = cur.next
if (cur.val > prev.val and cur.val > nxt.val) or (cur.val < prev.val and cur.val < nxt.val):
if first == -1:
first = i # the first critical point
else:
min_gap = min(min_gap, i - last) # distance to the previous critical point
last = i
prev = cur
cur = nxt
i = i + 1
if min_gap == float("inf"):
return [-1, -1] # fewer than two critical points
return [min_gap, last - first]
What the code is doing, line by line:
- The first and last nodes can't be critical (they lack a neighbour), so the walk starts at index 1 and stops before the tail.
- A node is critical when it's strictly above both neighbours or strictly below both.
- The minimum distance is always between two consecutive critical points, so comparing with
lastis enough. - The maximum distance is between the first and the last critical point.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Fewer than three nodes →
[-1, -1]. - Exactly one critical point →
[-1, -1]. - Equal neighbours are not critical (strictly greater/smaller is required).
Complexity: O(n) time, O(1) space.
Medium 708. Insert into a Sorted Circular Linked List Meta
Problem: Insert insertVal into a sorted circular linked list (given any node) so it stays sorted; return the given node (or the new one if empty).
Solve it on LeetCode ↗ (Premium)
head = [3,4,1], insertVal = 2[3,4,1,2]head = [], insertVal = 1[1]head = [1], insertVal = 0[1,0]Constraints: The number of nodes in the list is in the range [0, 5 * 10⁴] · -10⁶ ≤ Node.val, insertVal ≤ 10⁶
Hint 1
Where can the new value go in a sorted circle?
Hint 2
Handle the place where the list wraps from its largest to its smallest value.
From brute force to the pattern: Copying to an array and rebuilding is O(n) space; one walk is O(1).
Spot it: A linked-list walk with three cases: a gap prev ≤ x ≤ next; the wrap point (prev > next), where a new maximum or minimum belongs; or a full circle with all equal values (insert anywhere).
Why not another pattern? Finding the minimum first and then scanning works, but one loop handles every case.
Approach:
- Empty list: a single node pointing to itself.
- Walk
prev = head,cur = head.nextonce around. - Insert when
prev.val ≤ x ≤ cur.val, or at the wrap (prev.val > cur.val) whenx ≥ prev.valorx ≤ cur.val. - If no spot was found after a full circle (all equal), insert after
prev.
class Solution:
def insert(self, head: "Optional[Node]", insertVal: int) -> "Node":
node = Node(insertVal)
if head is None:
node.next = node # a circle of one
return node
prev, cur = head, head.next
while True:
if prev.val <= insertVal <= cur.val:
break # fits between two neighbours
if prev.val > cur.val and (insertVal >= prev.val or insertVal <= cur.val):
break # at the wrap point: new max or new min
prev, cur = cur, cur.next
if prev is head:
break # went all the way round (all values equal)
prev.next = node
node.next = cur
return head
What the code is doing, line by line:
- The wrap point is the only place where values drop; values beyond either end go there.
- If every value is equal, no condition fires before returning to
head, and any position is sorted.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty list.
- All values equal.
- New maximum or minimum.
Complexity: O(n) time, O(1) space.
Medium 1721. Swapping Nodes in a Linked List Amazon
Problem: Swap the values of the k-th node from the start and the k-th node from the end.
head = [1,2,3,4,5], k = 2[1,4,3,2,5]head = [7,9,6,6,7,8,3,0,9,5], k = 5[7,9,6,6,8,7,3,0,9,5]Constraints: The number of nodes in the list is n. · 1 ≤ k ≤ n ≤ 10⁵ · 0 ≤ Node.val ≤ 100
Hint 1
How do you find the k-th node from the end in one pass?
Hint 2
Keep two pointers k − 1 apart.
From brute force to the pattern: Copying to an array is O(n) space; two pointers are O(1).
Spot it: The k-th from the end is found with the gap technique: once a pointer has gone k steps, start a second pointer at the head; when the first reaches the end, the second is k from the end. Then swap the two values.
Why not another pattern? Counting the length first works too (two passes); copying to an array uses O(n) space.
Approach:
- Walk
k − 1steps from the head: that'sfirst. fast = first,second = head; advance both untilfastis the last node.- Swap
first.valandsecond.val.
class Solution:
def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
first = head
for _ in range(k - 1):
first = first.next # k-th from the start
fast, second = first, head
while fast.next:
fast = fast.next # keep a gap of k - 1 nodes
second = second.next
first.val, second.val = second.val, first.val # k-th from the end is `second`
return head
What the code is doing, line by line:
- When
fastis at the last node,secondisk − 1nodes behind it: the k-th from the end. - Swapping values is allowed and avoids relinking edge cases.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Both positions are the same node (odd middle).
- k = 1 (first and last).
- Two nodes.
Complexity: O(n) time, O(1) space.