Sentinel / Dummy Node
You get the head of a singly linked list and an integer val. Remove every node whose value equals val, and return the head of the list that is left (which may be empty).
Dummy Node
list whose head may change ➔ dummy.next = head ➔ rewire through prev, return dummy.next| 1 | from typing import Optional |
| 2 | |
| 3 | class ListNode: |
| 4 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | self.val = val |
| 6 | self.next = next |
| 7 | |
| 8 | def remove_elements(head: Optional[ListNode], val: int) -> Optional[ListNode]: |
| 9 | """Remove Linked List Elements: a dummy head makes deleting the real head ordinary. |
| 10 | Time: O(N) | Space: O(1) |
| 11 | """ |
| 12 | dummy = ListNode(0, head) # Every real node now has a previous node |
| 13 | prev = dummy |
| 14 | |
| 15 | # 1. Walk And Unlink Matching Nodes: |
| 16 | # prev always stays on the last kept node, so prev.next = prev.next.next removes the head the same way it removes any other node, and staying put after a removal lets the new prev.next be checked too. |
| 17 | while prev.next: |
| 18 | if prev.next.val == val: |
| 19 | prev.next = prev.next.next |
| 20 | else: |
| 21 | prev = prev.next |
| 22 | |
| 23 | # 2. Return Through The Dummy, Not head: |
| 24 | # head itself may have been removed, so dummy.next, not head, is always the current first real node. |
| 25 | return dummy.next |
prev.next has been checked and kept, and dummy.next is always the current head.The head is the only node without a previous node, which is why deleting or inserting at the head usually needs its own if. A dummy node placed before head gives every real node a previous node, so prev.next = prev.next.next removes the head like any other node. The sentinel does not have to be a node. It can be a virtual index, value or bar that is never stored in the data. It gives the first and the last element the same rule as every other element. Missing Ranges (LC 163) starts with prev = lower - 1 and ends with curr = upper + 1, so the gap before the first number and the gap after the last number use the same check as every gap between two numbers. Largest Rectangle in Histogram (LC 84) adds a bar of height 0 at the end, so the bars still on the stack are handled by the same rule. The trap is returning head: when the first node is removed, head still points at the deleted node, so return dummy.next.
See it run
Remove Linked List Elements: remove_elements([6, 6, 1, 2, 6, 3], val = 6)
prev dummyprev.next 6
Each circle is a node, and D is the dummy node. Arrows are next links. A removed node disappears from the picture and appears in the removed list.
prevthe last node that was keptprev.nextthe node that is checked nownew linkthe link that was just writtendummy.nextthe first kept node, which the function returns
dummy.next = head, so the first node has a previous node, the dummy D. prev starts at D.
Gotcha 1 of 5
- The trap
Gotcha 1: Returning
headinstead ofdummy.nextMistakereturn headFixreturn dummy.nextWhat happens:
headstill points at the first node of the input. When that node holdsval, the loop unlinks it fromdummy, butheaddoes not change. Returningheadreturns a list that starts with a deleted node.Example: head = [6, 6, 1, 2, 6, 3], val = 6Mistake: returns [6, 6, 1, 2, 3]Fix: returns [1, 2, 3]
- Every
nextpointer must point the other way, as in Reverse Linked List (LC 206): the head changes because the whole order flips, not because a node is added or removed at the front. See Linked List Reversal
Deep Dive
Why the head needs a node in front of it
A singly linked list stores only next links. To remove a node, you change the next link of the node before it. Every node has a node before it, except the head. So removing the head is a different operation: it changes the variable head, not a next link.
Without a dummy node, the code needs a separate loop for the head, such as while head and head.val == val: head = head.next, and then the usual loop for the rest. The same split appears when you insert at the front, or when you build a new list and do not know its first node yet.
A dummy node is one extra node placed before the head: dummy = ListNode(0, head). The value 0 is never read. Now every real node, including the head, has a node before it. One loop and one rule, prev.next = prev.next.next, remove any node.
Tracing [6, 6, 1, 2, 6, 3] with val = 6
prev starts at the dummy node. The loop runs while prev.next exists. At each step, prev.next is the node being checked, and prev is the last node that was kept.
The first 6 equals val, so prev.next = prev.next.next links the dummy to the second 6. prev does not move. The second 6 is now prev.next, and it is removed in the same way. The dummy is now linked to 1.
The values 1 and 2 differ from val, so prev moves forward over each. The next 6 is removed by linking 2 to 3. Then 3 is kept, and prev.next is None, so the loop stops. The result is [1, 2, 3]. The same code returns [1, 2, 3, 4, 5] for [1, 2, 6, 3, 4, 5, 6], which is Example 1 of Remove Linked List Elements (LC 203).
Two traps: staying put after a removal, and returning head
After prev.next = prev.next.next, the new prev.next has not been checked yet. So prev stays where it is, and the else branch moves it only when a node is kept. If prev moved after every step, two equal values in a row would leave the second one in the list. For [1, 6, 6, 2] and val = 6, that code returns [1, 6, 2] instead of [1, 2].
The second trap is the return value. After the first node is removed, the variable head still points at the removed node, and the removed node still links to the rest of the list. For the traced input, return head returns [6, 6, 1, 2, 3]. The dummy node always links to the first kept node, so return dummy.next returns [1, 2, 3].
If every node is removed, dummy.next is None, and the function returns an empty list. This case needs no extra code.
The sentinel does not have to be a node
A sentinel is an extra element that is not part of the real data. Its job is to remove a special case at an edge. In a linked list, the sentinel is a node. In an array or a range, it can be a virtual index, a virtual value or a virtual bar. The code never stores it. It only pretends that the element is there.
Missing Ranges (LC 163) gives a sorted array nums, a range lower to upper, and asks for every missing number, written as ranges. A gap before nums[0] and a gap after nums[-1] look like two extra cases. Set prev = lower - 1 before the loop, and let the final value of curr be upper + 1. Then each gap, including the first and the last, is the numbers strictly between prev and curr.
def process_ranges(nums: list[int], lower: int, upper: int) -> list[list[int]]: """Missing Ranges: virtual anchors make the first and last gap ordinary. Time: O(N) | Space: O(1) extra """ result = [] # Each item is one missing range [first, last] prev = lower - 1 # 1. Virtual left anchor: the number just before the range for i in range(len(nums) + 1): # 2. Virtual right anchor: on the final pass, curr is the number just after the range. curr = nums[i] if i < len(nums) else upper + 1 # 3. Uniform gap check: the numbers strictly between prev and curr are missing. if curr - prev > 1: result.append([prev + 1, curr - 1]) # 4. Advance: curr becomes the new left anchor. prev = curr return result Trace nums = [0, 1, 3, 50, 75], lower = 0, upper = 99. prev starts at -1. curr = 0 gives a difference of 1, so there is no gap. curr = 1 has no gap either. curr = 3 gives [2, 2]. curr = 50 gives [4, 49]. curr = 75 gives [51, 74]. The final curr = 100 gives [76, 99]. The result is [[2, 2], [4, 49], [51, 74], [76, 99]], which is Example 1 of Missing Ranges (LC 163).
An empty nums needs no extra code. The loop runs once with curr = upper + 1, and the result is [[lower, upper]]. The time is O(N) because each number is read once, and the extra space is O(1) besides the result.
Largest Rectangle in Histogram (LC 84) uses the same idea with a bar. After the last bar, the code adds one more bar of height 0. That bar is smaller than every real bar, so it pops all the bars still on the stack, and the code needs no second loop to flush them.
Two traps. First, the offsets: prev = lower or a final curr = upper drops the number at that end of the range, because the gap is computed as prev + 1 to curr - 1. Second, the special-case version: writing separate code for nums[0] and nums[-1] repeats the gap rule three times, and one wrong comparison in any copy gives a wrong answer.
The cost, and where a dummy node helps
The loop looks at each node once, so the time is O(N). The only extra memory is the dummy node and the variable prev, so the space is O(1).
Use a dummy node when the first node of the answer can change. Remove Nth Node From End of List (LC 19) can remove the head. Merge Two Sorted Lists (LC 21) does not know which list gives the first node. Remove Duplicates from Sorted List II (LC 82) removes every copy of a repeated value, including copies at the start.
In Remove Nth Node From End of List (LC 19), start both pointers at dummy, not at head. The trailing pointer must stop at the node before the one to remove. If the head is removed, only dummy is before it.
60-Second Pitch
"The head is the only node with no node before it, so removing it needs a special case. I remove that case with a dummy node: dummy = ListNode(0, head). Then prev starts at the dummy. While prev.next exists, I look at it. If its value equals val, I set prev.next = prev.next.next and keep prev where it is. If not, I move prev forward. At the end I return dummy.next, not head, because head may point at a removed node. The time is O(N) and the space is O(1)."
Common Questions
- What is the time and space complexity?
The time is
O(N), because the loop looks at each node once. The space isO(1): one dummy node and one pointer. The removal changes links in place.- Can you solve it without a dummy node?
Yes. First move
headforward withwhile head and head.val == val: head = head.next. Then run the sameprevloop fromhead. It gives the same result, but the removal rule is written twice. The dummy node writes it once.- Why return
dummy.nextand nothead? headis the variable that was passed in, and it never changes. If the first node is removed,headstill points at the removed node, and that node still links to later nodes.dummy.nextalways links to the current first node.- Why does
prevstay in place after a removal? The node that was after the removed node is now
prev.next, and it has not been checked. Ifprevmoved forward, the code would skip it. Two equal values in a row would leave the second one in the list.- Could you do it with recursion?
Yes. Set
head.nextto the result of the same function onhead.next. Then returnhead.nextifhead.val == val, orheadif not. It needs no dummy node, but the call stack usesO(N)space, and a long list can hit the recursion limit in Python.
SWE Reasoning
Spot it
- The head of the list can be removed, replaced or not known yet.
- The problem deletes nodes, merges lists, builds a new list or rewires a part of a list that can start at the head.
- Your first draft has an
ifthat only handles the head. - The function returns the new head.
The invariant, step by step
dummy = ListNode(0, head)puts a node before the head.prev = dummy.while prev.nextlooks at one node at a time.prev.nextis the node being checked.- If
prev.next.val == val,prev.next = prev.next.nextremoves it.prevstays, because the newprev.nextis not checked yet. - Otherwise,
prev = prev.nextkeeps the node and moves on. - Every node before
prev.nexthas been checked and kept, anddummy.nextis always the current first node. - When
prev.nextisNone,return dummy.nextreturns the list.
Traps
- Returning
head. If the first node was removed,headpoints at a node that is not in the list. - Moving
prevafter a removal. The newprev.nextis skipped, so two equal values in a row leave one behind. - Starting two pointers at
headinstead ofdummy. When the head must go, there is no node before it. - Using the dummy value. The value 0 in
dummyis not part of the list and must not be compared or returned.
Practice
Sentinel / Dummy Node
You get the head of a singly linked list and an integer val. Remove every node whose value equals val, and return the head of the list that is left (which may be empty).
Dummy Node
list whose head may change ➔ dummy.next = head ➔ rewire through prev, return dummy.next| 1 | from typing import Optional |
| 2 | |
| 3 | class ListNode: |
| 4 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | self.val = val |
| 6 | self.next = next |
| 7 | |
| 8 | def remove_elements(head: Optional[ListNode], val: int) -> Optional[ListNode]: |
| 9 | """Remove Linked List Elements: a dummy head makes deleting the real head ordinary. |
| 10 | Time: O(N) | Space: O(1) |
| 11 | """ |
| 12 | dummy = ListNode(0, head) # Every real node now has a previous node |
| 13 | prev = dummy |
| 14 | |
| 15 | # 1. Walk And Unlink Matching Nodes: |
| 16 | # prev always stays on the last kept node, so prev.next = prev.next.next removes the head the same way it removes any other node, and staying put after a removal lets the new prev.next be checked too. |
| 17 | while prev.next: |
| 18 | if prev.next.val == val: |
| 19 | prev.next = prev.next.next |
| 20 | else: |
| 21 | prev = prev.next |
| 22 | |
| 23 | # 2. Return Through The Dummy, Not head: |
| 24 | # head itself may have been removed, so dummy.next, not head, is always the current first real node. |
| 25 | return dummy.next |
prev.next has been checked and kept, and dummy.next is always the current head.The head is the only node without a previous node, which is why deleting or inserting at the head usually needs its own if. A dummy node placed before head gives every real node a previous node, so prev.next = prev.next.next removes the head like any other node. The sentinel does not have to be a node. It can be a virtual index, value or bar that is never stored in the data. It gives the first and the last element the same rule as every other element. Missing Ranges (LC 163) starts with prev = lower - 1 and ends with curr = upper + 1, so the gap before the first number and the gap after the last number use the same check as every gap between two numbers. Largest Rectangle in Histogram (LC 84) adds a bar of height 0 at the end, so the bars still on the stack are handled by the same rule. The trap is returning head: when the first node is removed, head still points at the deleted node, so return dummy.next.
See it run
Remove Linked List Elements: remove_elements([6, 6, 1, 2, 6, 3], val = 6)
prev dummyprev.next 6
Each circle is a node, and D is the dummy node. Arrows are next links. A removed node disappears from the picture and appears in the removed list.
prevthe last node that was keptprev.nextthe node that is checked nownew linkthe link that was just writtendummy.nextthe first kept node, which the function returns
dummy.next = head, so the first node has a previous node, the dummy D. prev starts at D.
Gotcha 1 of 5
- The trap
Gotcha 1: Returning
headinstead ofdummy.nextMistakereturn headFixreturn dummy.nextWhat happens:
headstill points at the first node of the input. When that node holdsval, the loop unlinks it fromdummy, butheaddoes not change. Returningheadreturns a list that starts with a deleted node.Example: head = [6, 6, 1, 2, 6, 3], val = 6Mistake: returns [6, 6, 1, 2, 3]Fix: returns [1, 2, 3]
- Every
nextpointer must point the other way, as in Reverse Linked List (LC 206): the head changes because the whole order flips, not because a node is added or removed at the front. See Linked List Reversal
Deep Dive
Why the head needs a node in front of it
A singly linked list stores only next links. To remove a node, you change the next link of the node before it. Every node has a node before it, except the head. So removing the head is a different operation: it changes the variable head, not a next link.
Without a dummy node, the code needs a separate loop for the head, such as while head and head.val == val: head = head.next, and then the usual loop for the rest. The same split appears when you insert at the front, or when you build a new list and do not know its first node yet.
A dummy node is one extra node placed before the head: dummy = ListNode(0, head). The value 0 is never read. Now every real node, including the head, has a node before it. One loop and one rule, prev.next = prev.next.next, remove any node.
Tracing [6, 6, 1, 2, 6, 3] with val = 6
prev starts at the dummy node. The loop runs while prev.next exists. At each step, prev.next is the node being checked, and prev is the last node that was kept.
The first 6 equals val, so prev.next = prev.next.next links the dummy to the second 6. prev does not move. The second 6 is now prev.next, and it is removed in the same way. The dummy is now linked to 1.
The values 1 and 2 differ from val, so prev moves forward over each. The next 6 is removed by linking 2 to 3. Then 3 is kept, and prev.next is None, so the loop stops. The result is [1, 2, 3]. The same code returns [1, 2, 3, 4, 5] for [1, 2, 6, 3, 4, 5, 6], which is Example 1 of Remove Linked List Elements (LC 203).
Two traps: staying put after a removal, and returning head
After prev.next = prev.next.next, the new prev.next has not been checked yet. So prev stays where it is, and the else branch moves it only when a node is kept. If prev moved after every step, two equal values in a row would leave the second one in the list. For [1, 6, 6, 2] and val = 6, that code returns [1, 6, 2] instead of [1, 2].
The second trap is the return value. After the first node is removed, the variable head still points at the removed node, and the removed node still links to the rest of the list. For the traced input, return head returns [6, 6, 1, 2, 3]. The dummy node always links to the first kept node, so return dummy.next returns [1, 2, 3].
If every node is removed, dummy.next is None, and the function returns an empty list. This case needs no extra code.
The sentinel does not have to be a node
A sentinel is an extra element that is not part of the real data. Its job is to remove a special case at an edge. In a linked list, the sentinel is a node. In an array or a range, it can be a virtual index, a virtual value or a virtual bar. The code never stores it. It only pretends that the element is there.
Missing Ranges (LC 163) gives a sorted array nums, a range lower to upper, and asks for every missing number, written as ranges. A gap before nums[0] and a gap after nums[-1] look like two extra cases. Set prev = lower - 1 before the loop, and let the final value of curr be upper + 1. Then each gap, including the first and the last, is the numbers strictly between prev and curr.
def process_ranges(nums: list[int], lower: int, upper: int) -> list[list[int]]: """Missing Ranges: virtual anchors make the first and last gap ordinary. Time: O(N) | Space: O(1) extra """ result = [] # Each item is one missing range [first, last] prev = lower - 1 # 1. Virtual left anchor: the number just before the range for i in range(len(nums) + 1): # 2. Virtual right anchor: on the final pass, curr is the number just after the range. curr = nums[i] if i < len(nums) else upper + 1 # 3. Uniform gap check: the numbers strictly between prev and curr are missing. if curr - prev > 1: result.append([prev + 1, curr - 1]) # 4. Advance: curr becomes the new left anchor. prev = curr return result Trace nums = [0, 1, 3, 50, 75], lower = 0, upper = 99. prev starts at -1. curr = 0 gives a difference of 1, so there is no gap. curr = 1 has no gap either. curr = 3 gives [2, 2]. curr = 50 gives [4, 49]. curr = 75 gives [51, 74]. The final curr = 100 gives [76, 99]. The result is [[2, 2], [4, 49], [51, 74], [76, 99]], which is Example 1 of Missing Ranges (LC 163).
An empty nums needs no extra code. The loop runs once with curr = upper + 1, and the result is [[lower, upper]]. The time is O(N) because each number is read once, and the extra space is O(1) besides the result.
Largest Rectangle in Histogram (LC 84) uses the same idea with a bar. After the last bar, the code adds one more bar of height 0. That bar is smaller than every real bar, so it pops all the bars still on the stack, and the code needs no second loop to flush them.
Two traps. First, the offsets: prev = lower or a final curr = upper drops the number at that end of the range, because the gap is computed as prev + 1 to curr - 1. Second, the special-case version: writing separate code for nums[0] and nums[-1] repeats the gap rule three times, and one wrong comparison in any copy gives a wrong answer.
The cost, and where a dummy node helps
The loop looks at each node once, so the time is O(N). The only extra memory is the dummy node and the variable prev, so the space is O(1).
Use a dummy node when the first node of the answer can change. Remove Nth Node From End of List (LC 19) can remove the head. Merge Two Sorted Lists (LC 21) does not know which list gives the first node. Remove Duplicates from Sorted List II (LC 82) removes every copy of a repeated value, including copies at the start.
In Remove Nth Node From End of List (LC 19), start both pointers at dummy, not at head. The trailing pointer must stop at the node before the one to remove. If the head is removed, only dummy is before it.
60-Second Pitch
"The head is the only node with no node before it, so removing it needs a special case. I remove that case with a dummy node: dummy = ListNode(0, head). Then prev starts at the dummy. While prev.next exists, I look at it. If its value equals val, I set prev.next = prev.next.next and keep prev where it is. If not, I move prev forward. At the end I return dummy.next, not head, because head may point at a removed node. The time is O(N) and the space is O(1)."
Common Questions
- What is the time and space complexity?
The time is
O(N), because the loop looks at each node once. The space isO(1): one dummy node and one pointer. The removal changes links in place.- Can you solve it without a dummy node?
Yes. First move
headforward withwhile head and head.val == val: head = head.next. Then run the sameprevloop fromhead. It gives the same result, but the removal rule is written twice. The dummy node writes it once.- Why return
dummy.nextand nothead? headis the variable that was passed in, and it never changes. If the first node is removed,headstill points at the removed node, and that node still links to later nodes.dummy.nextalways links to the current first node.- Why does
prevstay in place after a removal? The node that was after the removed node is now
prev.next, and it has not been checked. Ifprevmoved forward, the code would skip it. Two equal values in a row would leave the second one in the list.- Could you do it with recursion?
Yes. Set
head.nextto the result of the same function onhead.next. Then returnhead.nextifhead.val == val, orheadif not. It needs no dummy node, but the call stack usesO(N)space, and a long list can hit the recursion limit in Python.
SWE Reasoning
Spot it
- The head of the list can be removed, replaced or not known yet.
- The problem deletes nodes, merges lists, builds a new list or rewires a part of a list that can start at the head.
- Your first draft has an
ifthat only handles the head. - The function returns the new head.
The invariant, step by step
dummy = ListNode(0, head)puts a node before the head.prev = dummy.while prev.nextlooks at one node at a time.prev.nextis the node being checked.- If
prev.next.val == val,prev.next = prev.next.nextremoves it.prevstays, because the newprev.nextis not checked yet. - Otherwise,
prev = prev.nextkeeps the node and moves on. - Every node before
prev.nexthas been checked and kept, anddummy.nextis always the current first node. - When
prev.nextisNone,return dummy.nextreturns the list.
Traps
- Returning
head. If the first node was removed,headpoints at a node that is not in the list. - Moving
prevafter a removal. The newprev.nextis skipped, so two equal values in a row leave one behind. - Starting two pointers at
headinstead ofdummy. When the head must go, there is no node before it. - Using the dummy value. The value 0 in
dummyis not part of the list and must not be compared or returned.