Remove Nth Node From End of List
Keep two pointers a fixed gap apart; when the front one falls off the end, the back one stands right before the node to remove. Solved two ways, counting first in two passes and two pointers in one pass, both O(L), with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given the head of a singly linked list and a number position_from_end. Remove the node
at that position, counting from the end of the list, and return the head. The last node is at
position 1.
Example 1
Input: head = [8, 3, 6, 2, 9, 5], position_from_end = 2
Output: [8, 3, 6, 2, 5]
Counting from the end, 5 is 1st and 9 is 2nd. So 9 goes, and 2 now links straight to 5.
Example 2
Input: head = [4], position_from_end = 1
Output: []
The only node is removed, so the list is empty.
Example 3
Input: head = [1, 7], position_from_end = 2
Output: [7]
The 2nd from the end is the head itself. The new head is 7.
Constraints
Section titled “Constraints”- 1 ≤ L ≤ 30, where L is the number of nodes
- 0 ≤ Node.val ≤ 100
- 1 ≤ position_from_end ≤ L. Below, n is
position_from_end.
Clarifying questions
Section titled “Clarifying questions”- Is position_from_end always valid?
- Yes. 1 ≤ position_from_end ≤ L, so the node always exists.
- Can the head be the node to remove?
- Yes, when n = L. Then the answer starts at the second node.
- Do we know the length?
- No. We only get the head. Finding the length costs a full pass.
- Can it be done in one pass?
- Yes. That is the follow-up, and the two-pointer answer does it.
Spot the pattern
Section titled “Spot the pattern”“n-th from the end” is easy to turn into “k-th from the front” once you know the length. To skip the counting pass, keep two pointers a fixed distance apart. When the front one reaches the end, the back one is exactly where we need it. This is the two pointers pattern on a linked list.
Solutions
Section titled “Solutions”This problem has two approaches. Both are O(L), and the second one needs only one pass.
Count, then walk O(L), two passes
The idea
Section titled “The idea”Walk the whole list once to count its length L. The node to remove is number L − n + 1 from the front. To unlink it, we stand on the node before it and point that node past it. That node is L − n steps from a dummy node placed in front of the head.
The dummy node handles the case where the head itself must go: then we take 0 steps, and the dummy is the node before it.
Walkthrough
Section titled “Walkthrough”- length, node = 0, head
- while node: length += 1; node = node.next
- dummy = ListNode(0, head)
- previous = dummy
- for _ in range(length - position_from_end):
- previous = previous.next
- previous.next = previous.next.next
- return dummy.next
Pass 1 counts the nodes. Then the node to remove is number L − 2 + 1 from the front.
class Solution { // Count, then walk: find the length L, then step to the node before the // one to remove. Two passes, O(L) time and O(1) memory. public ListNode removeNthFromEnd(ListNode head, int positionFromEnd) { int length = 0; for (ListNode node = head; node != null; node = node.next) length++; // A dummy in front gives even the head a node before it. ListNode dummy = new ListNode(0, head); ListNode previous = dummy; // The target is node length - positionFromEnd + 1, so the node before it // is length - positionFromEnd steps from dummy. for (int i = 0; i < length - positionFromEnd; i++) previous = previous.next; previous.next = previous.next.next; return dummy.next; }}# Count, then walk: find the length L, then step to the node before the# one to remove. Two passes, O(L) time and O(1) memory.def remove_nth_from_end(head: ListNode | None, position_from_end: int) -> ListNode | None: length, node = 0, head while node: length += 1 node = node.next # A dummy in front gives even the head a node before it. dummy = ListNode(0, head) previous = dummy # The target is node length - position_from_end + 1, so the node before it # is length - position_from_end steps from dummy. for _ in range(length - position_from_end): previous = previous.next previous.next = previous.next.next return dummy.nextpublic class Solution{ // Count, then walk: find the length L, then step to the node before the // one to remove. Two passes, O(L) time and O(1) memory. public ListNode RemoveNthFromEnd(ListNode head, int positionFromEnd) { int length = 0; for (var node = head; node != null; node = node.next) length++; // A dummy in front gives even the head a node before it. var dummy = new ListNode(0, head); var previous = dummy; // The target is node length - positionFromEnd + 1, so the node before it // is length - positionFromEnd steps from dummy. for (int i = 0; i < length - positionFromEnd; i++) previous = previous.next; previous.next = previous.next.next; return dummy.next; }}// Count, then walk: find the length L, then step to the node before the// one to remove. Two passes, O(L) time and O(1) memory.func removeNthFromEnd(head *ListNode, positionFromEnd int) *ListNode { length := 0 for node := head; node != nil; node = node.Next { length++ } // A dummy in front gives even the head a node before it. dummy := &ListNode{Next: head} previous := dummy // The target is node length - positionFromEnd + 1, so the node before it // is length - positionFromEnd steps from dummy. for i := 0; i < length-positionFromEnd; i++ { previous = previous.Next } previous.Next = previous.Next.Next return dummy.Next}// Count, then walk: find the length L, then step to the node before the// one to remove. Two passes, O(L) time and O(1) memory.function removeNthFromEnd(head: ListNode | null, positionFromEnd: number): ListNode | null { let length = 0; for (let node = head; node; node = node.next) length++; // A dummy in front gives even the head a node before it. const dummy = new ListNode(0, head); let previous = dummy; // The target is node length - positionFromEnd + 1, so the node before it // is length - positionFromEnd steps from dummy. for (let i = 0; i < length - positionFromEnd; i++) previous = previous.next!; previous.next = previous.next!.next; return dummy.next;}1 Find the work that repeats
Two loops run one after the other. The first counts one node per run. The second moves
previousone node per run. Both are 1 step.- length, node = 0, head
- while node: length += 1; node = node.next
- dummy = ListNode(0, head)
- previous = dummy
- for _ in range(length - position_from_end):
- previous = previous.next
- previous.next = previous.next.next
- return dummy.next
2 Count the runs on Example 1
Example 1 has 6 nodes and n = 2. Counting takes 6 runs. Walking takes 6 − 2 = 4 runs.
6 + 4 = 10 runs.
loop count walk runs 6 4 total6 + 4 = 103 Write the count for any n
L + (L − n) = 2L − nCheck it with Example 1: 2 × 6 − 2 = 10. The worst case is n = 1, which is 2L − 1.
4 Keep only the part that grows fastest
Drop the 2 and the − n. What is left is L.
So the time is O(L).
S Space: count the extra memory the same way
The code keeps
length,node,dummyandprevious. That is fixed, for any L, so the space is O(1).lengthnodedummyprevious
Two pointers O(L), one pass
The idea
Section titled “The idea”Start slow and fast on a dummy node in front of the head. Move fast n + 1 nodes ahead. Then
move both, one node at a time, until fast falls off the end (becomes null).
The gap between them never changes: it stays n + 1. When fast is null, it is one step past the
last node. So slow is n + 1 steps before null, which is the node just before the one to remove.
Point slow.next past it.
The invariant: fast is always exactly n + 1 nodes after slow.
Walkthrough
Section titled “Walkthrough”- dummy = ListNode(0, head)
- slow = fast = dummy
- for _ in range(position_from_end + 1):
- fast = fast.next
- while fast:
- slow, fast = slow.next, fast.next
- slow.next = slow.next.next
- return dummy.next
Both pointers start on the dummy node D. First, fast runs 3 nodes ahead.
class Solution { // Two pointers: keep fast positionFromEnd + 1 nodes ahead of slow. When fast is // null, slow is right before the node to remove. One pass, O(L) time, O(1) memory. public ListNode removeNthFromEnd(ListNode head, int positionFromEnd) { // A dummy in front gives even the head a node before it. ListNode dummy = new ListNode(0, head); ListNode slow = dummy, fast = dummy; for (int i = 0; i <= positionFromEnd; i++) fast = fast.next; // The gap stays positionFromEnd + 1, so slow stops that many nodes before null. while (fast != null) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return dummy.next; }}# Two pointers: keep fast position_from_end + 1 nodes ahead of slow. When fast is# null, slow is right before the node to remove. One pass, O(L) time, O(1) memory.def remove_nth_from_end(head: ListNode | None, position_from_end: int) -> ListNode | None: # A dummy in front gives even the head a node before it. dummy = ListNode(0, head) slow = fast = dummy for _ in range(position_from_end + 1): fast = fast.next # The gap stays position_from_end + 1, so slow stops that many nodes before null. while fast: slow, fast = slow.next, fast.next slow.next = slow.next.next return dummy.nextpublic class Solution{ // Two pointers: keep fast positionFromEnd + 1 nodes ahead of slow. When fast is // null, slow is right before the node to remove. One pass, O(L) time, O(1) memory. public ListNode RemoveNthFromEnd(ListNode head, int positionFromEnd) { // A dummy in front gives even the head a node before it. var dummy = new ListNode(0, head); ListNode slow = dummy, fast = dummy; for (int i = 0; i <= positionFromEnd; i++) fast = fast.next; // The gap stays positionFromEnd + 1, so slow stops that many nodes before null. while (fast != null) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return dummy.next; }}// Two pointers: keep fast positionFromEnd + 1 nodes ahead of slow. When fast is// null, slow is right before the node to remove. One pass, O(L) time, O(1) memory.func removeNthFromEnd(head *ListNode, positionFromEnd int) *ListNode { // A dummy in front gives even the head a node before it. dummy := &ListNode{Next: head} slow, fast := dummy, dummy for i := 0; i <= positionFromEnd; i++ { fast = fast.Next } // The gap stays positionFromEnd + 1, so slow stops that many nodes before null. for fast != nil { slow, fast = slow.Next, fast.Next } slow.Next = slow.Next.Next return dummy.Next}// Two pointers: keep fast positionFromEnd + 1 nodes ahead of slow. When fast is// null, slow is right before the node to remove. One pass, O(L) time, O(1) memory.function removeNthFromEnd(head: ListNode | null, positionFromEnd: number): ListNode | null { // A dummy in front gives even the head a node before it. const dummy = new ListNode(0, head); let slow: ListNode = dummy; let fast: ListNode | null = dummy; for (let i = 0; i <= positionFromEnd; i++) fast = fast!.next; // The gap stays positionFromEnd + 1, so slow stops that many nodes before null. while (fast) { slow = slow.next!; fast = fast.next; } slow.next = slow.next!.next; return dummy.next;}1 Find the work that repeats
The first loop moves
fastonce per run. The second moves both pointers once per run. Each run is 1 step. The loops run one after the other.- dummy = ListNode(0, head)
- slow = fast = dummy
- for _ in range(position_from_end + 1):
- fast = fast.next
- while fast:
- slow, fast = slow.next, fast.next
- slow.next = slow.next.next
- return dummy.next
2 Count the runs on Example 1
Example 1 has 6 nodes and n = 2.
fastfirst moves 3 nodes ahead. Then both move 4 times, untilfastis null.3 + 4 = 7 runs.
loop fast ahead both move runs 3 4 total3 + 4 = 73 Write the count for any n
faststarts on the dummy and ends on null. That is L + 1 moves in total, split between the two loops:(n + 1) + (L − n) = L + 1Check it with Example 1: 6 + 1 = 7.
4 Keep only the part that grows fastest
Drop the + 1. What is left is L. It is the same Big O as counting first, but done in one pass.
So the time is O(L).
S Space: count the extra memory the same way
The code keeps
dummy,slowandfast. The space is O(1).dummyslowfast
Tested at the limits
Section titled “Tested at the limits”2,000 random lists of 1 to 8 nodes, removing any position from the tail to the head. Count, then walk and the two pointers gave the same list every time.
| Largest inputs | Result |
|---|---|
| 30 nodes (the most allowed), removing each of the 30 positions in turn, the head included. Both approaches. | Under 0.001 s each, and under 0.001 MB of extra memory: one dummy node and two pointers. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.