Skip to content

19. Remove Nth Node From End of List (Medium)

Problem

Given the head of a linked list, remove the n-th node from the end and return the new head. Follow-up: do it in one pass.

Example

  • head = [1,2,3,4,5], n = 2[1,2,3,5]
  • head = [1], n = 1[]
  • head = [1,2], n = 1[1]

LeetCode 19 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, two passes (count, then remove)

First pass: count length L. Second pass: advance L - n steps, then splice.

class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def remove_nth_from_end(head, n):
dummy = ListNode(0, head)
L = 0
cur = head
while cur: # L1: first pass, count length
L += 1 # L2: O(1) per step
cur = cur.next
cur = dummy
for _ in range(L - n): # L3: second pass, advance L-n steps
cur = cur.next # L4: O(1) per step
cur.next = cur.next.next # L5: O(1) splice
return dummy.next

Where the time goes, line by line

Variables: L = number of nodes in the list, n = the parameter (position from end).

LinePer-call costTimes executedContribution
L1-L2 (count pass)O(1)O(1)LO(L)O(L)
L3-L4 (advance pass)O(1)O(1)L - nO(L)O(L) ← dominates
L5 (splice)O(1)O(1)1O(1)O(1)

Both passes are O(L)O(L); together they give O(2L)O(2L) = O(L)O(L). The second pass only goes L - n steps, but in the worst case (n = 1) that’s still L - 1 steps.

Complexity

  • Time: O(L)O(L). Two passes, each O(L)O(L).
  • Space: O(1)O(1).

Works. Not one-pass.

Approach 2: Recursive removal counting from the end

Recurse to the end, then decrement a counter on the way back up; at n == 0, splice the previous node.

def remove_nth_from_end(head, n):
dummy = ListNode(0, head)
def rec(node):
if not node: # L1: base case
return 0
k = rec(node.next) + 1 # L2: recurse deeper, O(1) on return
if k == n + 1:
node.next = node.next.next # L3: O(1) splice at right depth
return k
rec(dummy)
return dummy.next

Where the time goes, line by line

Variables: L = number of nodes in the list, n = the parameter (position from end).

LinePer-call costTimes executedContribution
L1 (base case)O(1)O(1)1O(1)O(1)
L2 (recurse)O(1)O(1) per frameL + 1O(L)O(L) ← dominates (stack depth)
L3 (splice)O(1)O(1)1O(1)O(1)

The recursion unwinds L + 1 frames (including the dummy). Each frame does O(1)O(1) work; all cost is in the stack depth.

Complexity

  • Time: O(L)O(L).
  • Space: O(L)O(L) recursion depth.

Elegant but uses stack proportional to list length.

Approach 3: Two-pointer offset (optimal one-pass)

Advance fast by n + 1 steps, then walk both pointers together. When fast falls off, slow sits one before the node to remove.

def remove_nth_from_end(head, n):
dummy = ListNode(0, head)
slow = fast = dummy
for _ in range(n + 1): # L1: advance fast n+1 steps
fast = fast.next # L2: O(1) per step
while fast: # L3: walk both until fast falls off
slow = slow.next # L4: O(1) per step
fast = fast.next # L5: O(1) per step
slow.next = slow.next.next # L6: O(1) splice
return dummy.next

Where the time goes, line by line

Variables: L = number of nodes in the list, n = the parameter (position from end).

LinePer-call costTimes executedContribution
L1-L2 (offset phase)O(1)O(1)n + 1O(n)O(n)
L3-L5 (walk phase)O(1)O(1)L - nO(L)O(L) ← dominates
L6 (splice)O(1)O(1)1O(1)O(1)

Total steps = (n + 1) + (L - n) = L + 1, so exactly one pass over the list. The dummy node ensures slow has a valid .next even when removing the head (n == L).

Complexity

  • Time: O(L)O(L). One pass.
  • Space: O(1)O(1).

Why the dummy node matters

When n == L (removing the head), the slow pointer needs to land “one before the head.” A dummy sentinel makes “one before the head” a real node, so the splice logic is uniform regardless of whether we’re removing the head.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Test cases

# Quick smoke tests, paste into a REPL or save as test_019.py and run.
# Uses the two-pointer offset approach (Approach 3).
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def to_list(head):
out = []
while head:
out.append(head.val)
head = head.next
return out
def from_list(vals):
dummy = ListNode()
cur = dummy
for v in vals:
cur.next = ListNode(v)
cur = cur.next
return dummy.next
def remove_nth_from_end(head, n):
dummy = ListNode(0, head)
slow = fast = dummy
for _ in range(n + 1):
fast = fast.next
while fast:
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.next
def _run_tests():
# Example: remove 2nd from end of [1,2,3,4,5] -> [1,2,3,5]
assert to_list(remove_nth_from_end(from_list([1,2,3,4,5]), 2)) == [1,2,3,5]
# Single element, remove it
assert to_list(remove_nth_from_end(from_list([1]), 1)) == []
# Two elements, remove last
assert to_list(remove_nth_from_end(from_list([1,2]), 1)) == [1]
# Two elements, remove first (n == L)
assert to_list(remove_nth_from_end(from_list([1,2]), 2)) == [2]
# Remove head of longer list
assert to_list(remove_nth_from_end(from_list([1,2,3,4,5]), 5)) == [2,3,4,5]
print("all tests pass")
if __name__ == "__main__":
_run_tests()

Summary

ApproachTimeSpacePasses
Two-pass count + removeO(L)O(L)O(1)O(1)2
RecursiveO(L)O(L)O(L)O(L) stack1
Two-pointer offsetO(L)O(L)O(1)O(1)1

The offset-n two-pointer trick generalizes to “find the k-th from end” and variants.

  • Linked Lists, two-pointer distance pattern with dummy head