Return the head of a singly linked list after removing the kth node from the end of it.
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
Two friends grab the same rope. The first walks ahead until there are exactly k knots of rope between them. Then they walk together at the same speed. The moment the leader reaches the end of the rope, the follower is standing exactly k knots from the end — without anyone counting the total length. That fixed gap is the whole trick.
We can divide this problem into two objectives:
Let’s first understand how node removal works. Consider the example below, where we need to remove node b. To do this, we need access to the node preceding it (node a), so we can redirect the pointer of node a to skip over node b. This ensures node b is no longer reachable through linked list traversal:
This indicates we need to find the node directly before the kth last node in order to remove it.
A naive solution to this problem is to first obtain the length of the linked list (n) by traversing it. Then, use this length to determine the number of steps required to arrive at the node before the kth last node, which is just n - k - 1 steps. This solution involves two for-loops, but is there a cleaner way to approach this problem?
The challenge with navigating a singly linked list in a single for-loop is that as we traverse, it’s hard to tell how far we are from the final node. The only way we’d know this is when we reach the final node itself, since its next node is null. How can we make use of this information?
Consider using two pointers instead of one. Could we create a scenario where, by the time one pointer reaches the end of the linked list, another pointer is positioned before the kth last node? Let’s explore this logic using the following example:
We denote the first pointer as leader and the pointer that follows it as trailer. When the leader pointer reaches the last node of the linked list, we want the trailer pointer to end up at node 4 (the node right before the kth last node) to prepare for deletion. In other words, the leader should be k nodes in front of the trailer when the leader reaches the last node.
To achieve this, we can start by advancing the leader pointer through the linked list for k steps. When the leader pointer is k nodes ahead of the trailer, we can advance both pointers together until the leader reaches the last node. This process will be explained in more detail soon.
However, there’s an important edge case to consider first: what if the head itself is the node we need to remove? In this case, there’s no node before the head, so we cannot perform the removal, as mentioned earlier. To circumvent this, we can create a dummy node, place it before the head node, and start our traversal from there.
Let’s now try incorporating our strategy into the example.
First, advance the leader pointer k (2) times so it’s k nodes ahead of the trailer pointer:
With the leader k nodes ahead, we can move both the trailer and leader pointers until the leader reaches the last node:
With the trailer pointer at the ideal position, we can remove node 7:
After this removal, we just return dummy.next, which points at the head of the modified linked list.
from ds import ListNode
def remove_kth_last_node(head: ListNode, k: int) -> ListNode:
# A dummy node to ensure there's a node before 'head' in case we need to remove
# the head node.
dummy = ListNode(-1)
dummy.next = head
trailer = leader = dummy
# Advance 'leader' k steps ahead.
for _ in range(k):
leader = leader.next
# If k is larger than the length of the linked list, no node needs to be
# removed.
if not leader:
return head
# Move 'leader' to the end of the linked list, keeping 'trailer' k nodes behind.
while leader.next:
leader = leader.next
trailer = trailer.next
# Remove the kth node from the end.
trailer.next = trailer.next.next
return dummy.nextListNode* removeKthLastNode(ListNode* head, int k) {
ListNode dummy(0); // sentinel before the head
dummy.next = head;
ListNode* lead = &dummy;
ListNode* trail = &dummy;
for (int i = 0; i < k; i++) { // open a gap of k nodes
lead = lead->next;
}
while (lead->next != nullptr) { // move together to the end
lead = lead->next;
trail = trail->next;
}
trail->next = trail->next->next; // skip the target node
return dummy.next;
}public ListNode removeKthLastNode(ListNode head, int k) {
ListNode dummy = new ListNode(0); // sentinel before the head
dummy.next = head;
ListNode lead = dummy;
ListNode trail = dummy;
for (int i = 0; i < k; i++) { // open a gap of k nodes
lead = lead.next;
}
while (lead.next != null) { // move together to the end
lead = lead.next;
trail = trail.next;
}
trail.next = trail.next.next; // skip the target node
return dummy.next;
}Time complexity: The time complexity of remove_kth_last_node is . This is because the algorithm first traverses at most nodes of the linked list, and then two pointers traverse the linked list at most once each.
Space complexity: The space complexity is .
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
trail (lands before target) lead (k ahead)
| Phase | trail at | lead at | lead.next |
|---|---|---|---|
| after gap | D | 2 | 3 |
| step | 1 | 3 | 4 |
| step | 2 | 4 | 5 |
| step (stop) | 3 | 5 | null |
trail lead removed
D sits before the head. lead = trail = D.
lead k = 5 steps: D→1→2→3→4→5. It lands on the last node; trail is still on the dummy, and lead.next is already null.
lead.next != null is false right away, so neither pointer moves. trail stays parked on the dummy, one step before the head.
trail.next = trail.next.next makes the dummy skip node 1. Return dummy.next = node 2. 🎉
| Phase | trail at | lead at | lead.next |
|---|---|---|---|
| after gap (5 steps) | D | 5 | null |
| walk 0× → splice | D | 5 | null |
Contrast with Dry run 1: there trail walked all the way to node 3 before splicing. Here k equals the length, so lead opens the gap right onto the last node and the walk loop never runs — trail stays on the dummy and dummy.next is rewired to node 2. This is exactly why the dummy head erases the “what if we delete the head?” special case.
Both runs step exactly as the tables above. Python-specific: open the gap with for _ in range(k), walk with while lead.next is not None, and always return dummy.next (never head).
Same steps. C++-specific: ListNode dummy(0); dummy.next = head; the loop condition is while (lead->next != nullptr); return dummy.next.
Same steps. Java-specific: ListNode dummy = new ListNode(0); dummy.next = head; loop while lead.next != null; return dummy.next.