Problem
Given a linked list, remove every node that has a node with a strictly greater value somewhere to its right. A node survives only if it is at least as large as every node that comes after it.
[5,2,13,3,8]
- Input: head = [5, 2, 13, 3, 8]
- Output: [13, 8]
- Explanation: 5 and 2 are removed because 13 comes after them; 3 is removed because 8 comes after it.
Counter-example: [1, 1, 1] โ [1, 1, 1]. Equal values do not trigger removal โ only a strictly greater value to the right causes a node to be removed.
Intuition
A node survives if and only if it is greater than or equal to every node to its right. The challenge is that we need to look forward in the list to make a decision about the current node, which is awkward for a standard left-to-right traversal. Two approaches flip this: recursion naturally processes the right side before the left, and reversing the list lets us scan right-to-left while tracking a running maximum.
Approach 1 โ Recursion
Process the rest of the list first. Once the right sublist is fixed, check if the current node is dominated by the head of that fixed list. If so, skip the current node.
- Base case: if head is null, return null.
- Recursively fix the rest of the list:
head.next = removeNodes(head.next). - After the recursive call returns,
head.nextis already the largest node reachable from the right. - If
head.nextexists and its value exceeds the current node's value, skip the current node by returninghead.next. - Otherwise, keep the current node and return it.
1def removeNodes(self, head: Optional[ListNode]) -> Optional[ListNode]:
2 if not head:
3 return None
4
5 # fix the right side first, then evaluate the current node
6 head.next = self.removeNodes(head.next)
7
8 if head.next and head.next.val > head.val:
9 return head.next # current node is dominated; skip it
10 return headTime: O(n) โ each node is visited exactly once on the way down the call stack.
Space: O(n) โ the recursion stack grows one frame per node.
Approach 2 โ Reverse + Monotonic Filter
Reverse the list so that what was originally to the right is now to the left. Then scan left-to-right tracking the maximum value seen so far: any node smaller than the running maximum would have been dominated in the original order, so remove it. Reverse back.
- Reverse the linked list in-place.
- Initialize
running_maxto the head's value (the original tail). - Walk forward: if the next node's value is less than
running_max, skip it; otherwise updaterunning_maxand advance. - Reverse the filtered list back to restore original order.
1def removeNodes(self, head: Optional[ListNode]) -> Optional[ListNode]:
2 def reverse_list(node: Optional[ListNode]) -> Optional[ListNode]:
3 prev = None
4 curr = node
5 while curr:
6 next_node = curr.next
7 curr.next = prev
8 prev = curr
9 curr = next_node
10 return prev
11
12 head = reverse_list(head)
13
14 running_max = head.val # initialize from actual head to handle negative values
15 current = head
16 while current.next:
17 if current.next.val < running_max:
18 current.next = current.next.next # node was dominated in original order
19 else:
20 running_max = current.next.val
21 current = current.next
22
23 return reverse_list(head)Time: O(n) โ two reversal passes and one filter pass, each O(n).
Space: O(1) โ all operations are in-place with only a few pointer variables.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Recursion | O(n) | O(n) | Cleaner code when stack depth isn't a concern (lists up to ~10k nodes) |
| Reverse + Filter | O(n) | O(1) | Preferred for very long lists where call-stack overflow is a risk |
Common Mistakes
- Bottom-up vs top-down recursion: The recursive fix must happen on
head.nextbefore the comparison โ callingremoveNodeson the already-modifiedhead.nextis what makes the comparison valid. Doing the comparison first produces wrong answers. - Equal values trigger removal: Only a strictly greater value to the right removes a node. A node equal to all future nodes should stay. Using
>=in the comparison instead of>incorrectly removes ties. - Initializing running max to 0: If node values can be negative (valid per constraints), initializing
running_max = 0instead ofhead.valwill incorrectly remove all negative-valued nodes at the start of the reversed list. - Overwriting next before saving it during reversal: In the iterative reverse,
curr.next = prevmust happen afternext_node = curr.nextโ otherwise the forward reference is lost. - Reversing the wrong list: The second reversal should operate on the filtered list (the result of the filter pass), not the pre-filter reversed list saved separately.
Related Problems
Reverse Linked Listโ core operation used in Approach 2; understanding in-place reversal is a prerequisiteRemove Linked List Elementsโ same "unlink a node by updating the previous node's next pointer" mechanic, without the right-side dependencyRemove Nth Node From End of Listโ also requires reasoning about positions relative to the end of the listDelete the Middle Node of a Linked Listโ another in-place node deletion with a traversal-based trickOdd Even Linked Listโ rearranging nodes by selectively relinking, using the same pointer manipulation pattern