209 lines
8.7 KiB
YAML
209 lines
8.7 KiB
YAML
title: Remove Nth Node From End of List
|
|
slug: remove-nth-node-from-end-of-list
|
|
difficulty: medium
|
|
leetcode_id: 19
|
|
leetcode_url: https://leetcode.com/problems/remove-nth-node-from-end-of-list/
|
|
categories:
|
|
- linked-lists
|
|
- two-pointers
|
|
patterns:
|
|
- slug: two-pointers
|
|
is_optimal: true
|
|
- slug: fast-slow-pointers
|
|
is_optimal: false
|
|
|
|
function_signature: "def remove_nth_from_end(head: ListNode, n: int) -> ListNode:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { head: [1, 2, 3, 4, 5], n: 2 }
|
|
expected: [1, 2, 3, 5]
|
|
- input: { head: [1], n: 1 }
|
|
expected: []
|
|
- input: { head: [1, 2], n: 1 }
|
|
expected: [1]
|
|
hidden:
|
|
- input: { head: [1, 2], n: 2 }
|
|
expected: [2]
|
|
- input: { head: [1, 2, 3], n: 3 }
|
|
expected: [2, 3]
|
|
- input: { head: [1, 2, 3, 4, 5], n: 5 }
|
|
expected: [2, 3, 4, 5]
|
|
- input: { head: [1, 2, 3, 4, 5], n: 1 }
|
|
expected: [1, 2, 3, 4]
|
|
|
|
description: |
|
|
Given the `head` of a linked list, remove the `n`<sup>th</sup> node from the end of the list and return its head.
|
|
|
|
constraints: |
|
|
- The number of nodes in the list is `sz`
|
|
- `1 <= sz <= 30`
|
|
- `0 <= Node.val <= 100`
|
|
- `1 <= n <= sz`
|
|
|
|
examples:
|
|
- input: "head = [1,2,3,4,5], n = 2"
|
|
output: "[1,2,3,5]"
|
|
explanation: "The 2nd node from the end is 4. After removing it, the list becomes [1,2,3,5]."
|
|
- input: "head = [1], n = 1"
|
|
output: "[]"
|
|
explanation: "The list has only one node, and we remove it. The result is an empty list."
|
|
- input: "head = [1,2], n = 1"
|
|
output: "[1]"
|
|
explanation: "The 1st node from the end is 2. After removing it, the list becomes [1]."
|
|
|
|
explanation:
|
|
intuition: |
|
|
The challenge here is that we're counting from the *end* of the list, but linked lists only allow forward traversal. How do we know which node is the n<sup>th</sup> from the end without first knowing the total length?
|
|
|
|
Think of it like this: imagine two people walking along the same path at the same speed. If the first person gets a **head start of exactly `n` steps**, then when they reach the end, the second person will be exactly `n` steps behind — standing at the n<sup>th</sup> position from the end.
|
|
|
|
This is the essence of the **two-pointer technique** for linked lists. We use two pointers: a `fast` pointer that advances `n` steps ahead, and a `slow` pointer that starts at the beginning. When we move both pointers together one step at a time, by the time `fast` reaches the end, `slow` will be pointing to the node just *before* the one we want to remove.
|
|
|
|
The key insight is that the **gap between the two pointers stays constant** throughout the traversal. This gap acts as our "measuring stick" to find the target node in a single pass.
|
|
|
|
approach: |
|
|
We solve this using the **Two-Pointer (Fast and Slow) Approach** with a dummy node:
|
|
|
|
**Step 1: Create a dummy node**
|
|
|
|
- Create a `dummy` node that points to `head`
|
|
- This handles edge cases where we need to remove the first node
|
|
- Initialise both `slow` and `fast` pointers to the dummy node
|
|
|
|
|
|
|
|
**Step 2: Advance the fast pointer**
|
|
|
|
- Move `fast` forward by `n + 1` steps
|
|
- This creates a gap of `n + 1` nodes between `slow` and `fast`
|
|
- The extra step ensures `slow` stops at the node *before* the target (so we can delete it)
|
|
|
|
|
|
|
|
**Step 3: Move both pointers together**
|
|
|
|
- Move `slow` and `fast` one step at a time until `fast` reaches the end (`None`)
|
|
- When `fast` is `None`, `slow` is pointing to the node immediately before the one to remove
|
|
|
|
|
|
|
|
**Step 4: Remove the target node**
|
|
|
|
- Skip over the target node: `slow.next = slow.next.next`
|
|
- This effectively removes the n<sup>th</sup> node from the end
|
|
|
|
|
|
|
|
**Step 5: Return the new head**
|
|
|
|
- Return `dummy.next` (not `head`, as the original head might have been removed)
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting the Dummy Node
|
|
description: |
|
|
When removing the **first node** of the list (n equals the list length), there is no "previous node" to update.
|
|
|
|
For example, with `[1,2,3]` and `n = 3`, we need to remove node `1`. Without a dummy node, we'd have no way to update what points to the head.
|
|
|
|
Using a dummy node that points to `head` means `slow` can stop at the dummy and still correctly execute `slow.next = slow.next.next` to remove the first real node.
|
|
wrong_approach: "Starting slow at head directly"
|
|
correct_approach: "Use a dummy node pointing to head, start slow at dummy"
|
|
|
|
- title: Off-by-One Error in Gap
|
|
description: |
|
|
If you advance `fast` by only `n` steps (instead of `n + 1`), `slow` will end up pointing *at* the target node rather than the node before it.
|
|
|
|
Since we need to modify the `next` pointer of the node *before* the target, we must ensure `slow` stops one position earlier.
|
|
|
|
Moving `fast` by `n + 1` steps creates the correct gap so `slow.next` is the node to remove.
|
|
wrong_approach: "Advance fast by n steps"
|
|
correct_approach: "Advance fast by n + 1 steps"
|
|
|
|
- title: Two-Pass Approach
|
|
description: |
|
|
A naive solution counts all nodes first to find the list length, then traverses again to find the target node. While correct, this requires **two passes** through the list.
|
|
|
|
The two-pointer technique achieves the same result in a **single pass** by maintaining a fixed gap between pointers. This is more elegant and meets the follow-up challenge.
|
|
wrong_approach: "Count nodes first, then traverse again"
|
|
correct_approach: "Single pass with two pointers maintaining n-step gap"
|
|
|
|
key_takeaways:
|
|
- "**Two-pointer gap technique**: Maintaining a fixed gap between pointers lets you measure distances from the end in a single pass"
|
|
- "**Dummy node pattern**: A dummy node simplifies edge cases when the head itself might be modified or removed"
|
|
- "**Off-by-one awareness**: When deleting a node, you need access to its predecessor, so adjust your gap accordingly"
|
|
- "**Foundation for linked list problems**: This pattern extends to finding the middle node, detecting cycles, and other linked list operations"
|
|
|
|
time_complexity: "O(n). We traverse the list exactly once with two pointers."
|
|
space_complexity: "O(1). We only use a constant number of pointers (`dummy`, `slow`, `fast`) regardless of list size."
|
|
|
|
solutions:
|
|
- approach_name: Two Pointers with Dummy Node
|
|
is_optimal: true
|
|
code: |
|
|
class ListNode:
|
|
def __init__(self, val=0, next=None):
|
|
self.val = val
|
|
self.next = next
|
|
|
|
def remove_nth_from_end(head: ListNode, n: int) -> ListNode:
|
|
# Dummy node handles edge case of removing the head
|
|
dummy = ListNode(0, head)
|
|
slow = dummy
|
|
fast = dummy
|
|
|
|
# Advance fast pointer by n + 1 steps to create the gap
|
|
for _ in range(n + 1):
|
|
fast = fast.next
|
|
|
|
# Move both pointers until fast reaches the end
|
|
while fast is not None:
|
|
slow = slow.next
|
|
fast = fast.next
|
|
|
|
# slow.next is the node to remove; skip over it
|
|
slow.next = slow.next.next
|
|
|
|
# Return the new head (dummy.next in case head was removed)
|
|
return dummy.next
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the list.
|
|
|
|
**Space Complexity:** O(1) — Only three pointers used.
|
|
|
|
We create a gap of `n + 1` between the two pointers. When `fast` reaches the end, `slow` is positioned at the node before the target. We then simply skip over the target node to remove it from the list.
|
|
|
|
- approach_name: Two Pass (Count Length)
|
|
is_optimal: false
|
|
code: |
|
|
def remove_nth_from_end(head: ListNode, n: int) -> ListNode:
|
|
# First pass: count total nodes
|
|
length = 0
|
|
current = head
|
|
while current:
|
|
length += 1
|
|
current = current.next
|
|
|
|
# Calculate position from the start (0-indexed)
|
|
target_index = length - n
|
|
|
|
# Edge case: removing the head
|
|
if target_index == 0:
|
|
return head.next
|
|
|
|
# Second pass: find the node before the target
|
|
current = head
|
|
for _ in range(target_index - 1):
|
|
current = current.next
|
|
|
|
# Remove the target node
|
|
current.next = current.next.next
|
|
|
|
return head
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Two passes through the list.
|
|
|
|
**Space Complexity:** O(1) — Only uses a counter and pointer.
|
|
|
|
This approach first counts the total number of nodes, calculates the position from the start, then traverses again to find and remove the target. While correct and still O(n), it requires two passes instead of one. The two-pointer approach is more elegant and solves the follow-up challenge.
|