Delete the tail node of a linked list
You're warming up in a technical interview and the interviewer draws a chain of boxes on the whiteboard. "Here's a singly linked list," they say. "Remove the last node and return the head." It sounds straightforward, but this problem is a litmus test for how well you handle pointer manipulation and edge cases. It's also known as "Remove Last Node from Linked List" on other platforms, and it appears frequently as a warm-up before harder linked list questions.
TL;DR
Traverse the singly linked list to find the second-to-last node, then set its next pointer to null. Handle edge cases first: return null for an empty list or a single-node list. This runs in O(n) time and O(1) space. The critical detail is checking current.next.next (not current.next) so you stop one node before the tail.
Why This Problem Matters
Deleting the tail of a linked list is one of the first problems you should master when learning linked lists. It tests three skills that interviewers care about: traversing a list to a specific position, handling null-pointer edge cases cleanly, and modifying pointers to reshape the list. Once you can confidently delete the tail, you'll be ready for tougher operations like removing the Nth node from the end or reversing sublists.
Linked Lists: A Quick Refresher
A singly linked list is a sequence of nodes where each node holds a value and a reference to the next node. The last node in the chain points to null, marking the end.
Loading visualization...
A few things to keep in mind:
- Head: The first node, your only entry point into the list
- Tail: The last node, which points to
null - One-way traffic: You can only move forward through the list, never backward
- No random access: To reach the 4th node, you must pass through nodes 1, 2, and 3
This one-directional nature is exactly what makes tail deletion interesting. You need the node before the tail, but the tail has no way of telling you who came before it.
Understanding the Problem
Given: The head node of a singly linked list Task: Delete the tail (last) node Return: The head of the modified list
Here's the transformation for the list [1, 2, 3, 4, 5]:
Loading visualization...
After deleting the tail:
Loading visualization...
Node 5 was the tail, so we remove it. Node 4 becomes the new tail, and its next pointer now points to null.
Edge Cases
Before writing any traversal logic, handle the boundary conditions:
- Empty list (
nullhead): Nothing to delete, returnnull - Single node: The head is also the tail, so deleting the tail means returning
null - Two nodes: Delete the second node and return the head
Loading visualization...
For a single-node list, deleting the tail removes the only node. The result is an empty list.
Loading visualization...
For a two-node list, deleting the tail leaves just the head node:
Loading visualization...
Solution Approach
The strategy is simple: walk through the list until you find the node whose next node is the tail. Then cut the link.
Here's the plan:
- If the list is empty or has one node, return
null - Start a pointer
currentat the head - Advance
currentuntilcurrent.next.nextisnull - Set
current.next = nullto remove the tail - Return
head
The key insight is in step 3. By checking current.next.next instead of current.next, you stop at the second-to-last node rather than the tail itself. This gives you the handle you need to sever the connection.
Walking Through an Example
Let's trace through [1, 2, 3, 4, 5] step by step.
Step 1: Start at node 1. Check current.next.next (node 3) - not null, so keep going.
Loading visualization...
Step 2: Move to node 2. Check current.next.next (node 4) - not null, keep going.
Loading visualization...
Step 3: Move to node 3. Check current.next.next (node 5) - not null, keep going.
Loading visualization...
Step 4: Move to node 4. Check current.next.next - it's null. We've found the second-to-last node.
Loading visualization...
Now set current.next = null. Node 4's pointer no longer references node 5, effectively removing it from the list. Return head.
Implementation
Prefer a different language? Jump to solutions in other languages.
public class Solution {
public ListNode deleteTail(ListNode head) {
// Handle empty list and single-node list
if (head == null || head.next == null) {
return null;
}
// Traverse to the second-to-last node
ListNode current = head;
while (current.next.next != null) {
current = current.next;
}
// Sever the link to the tail
current.next = null;
// Return the original head
return head;
}
}
Let's walk through the code with [1, 2]:
headis node 1,head.nextis node 2 - neither is null, so we skip the guard clausecurrentstarts at node 1current.next.nextisnull(node 2 is the tail) - the while loop doesn't executecurrent.next = nullremoves node 2- Return node 1
That two-node case is worth tracing because it's the smallest input where traversal actually matters.
Complexity Analysis
Time Complexity: O(n)
We traverse the list once from head to the second-to-last node. Each node is visited at most once, so the time grows linearly with the list length. For a list with 10,000 nodes (the upper bound from the constraints), that's 9,999 comparisons.
Space Complexity: O(1)
We use a single pointer variable current regardless of list size. No additional data structures, no recursion, no extra memory.
Why Not O(1) Time?
A natural follow-up question in interviews is whether you can do better. With a singly linked list, O(n) is the best you can achieve for tail deletion. You must reach the second-to-last node, and there's no shortcut. With a doubly linked list or a tail pointer, you could achieve O(1), but the problem specifies a singly linked list.
Common Pitfalls
-
Using
current.next != nullas the loop condition: This stops at the tail, not the node before it. You'd lose the reference you need to sever the link.// Wrong - stops at the tail itself while (current.next != null) { current = current.next; } // current is now the tail, but who points to current? You don't know. -
Forgetting the single-node edge case: If you only check
head == null, a single-node list will crash when you accesshead.next.next. -
Not returning the original head: After deleting the tail, the head hasn't changed. Make sure you return
head, notcurrent. -
Null pointer on an empty list: Accessing
head.nextwhenheadisnullthrows an exception. Always guard against null first.
Interview Tips
When you encounter this problem in an interview:
-
Start with edge cases: Tell the interviewer "I'll handle the empty list and single-node cases first." This shows disciplined thinking.
-
Draw the list: Sketch 3-4 nodes on the whiteboard. Circle the second-to-last node and explain why it's the key.
-
Explain the loop condition: Say "I'm checking
current.next.nextbecause I need to stop one node before the tail." This is the core insight, and articulating it clearly demonstrates understanding. -
Mention the O(1) alternative: After presenting your solution, note that a doubly linked list or maintaining a tail pointer would allow O(1) deletion. This shows you understand the broader design trade-offs.
-
Anticipate follow-ups: "How would you delete the Nth node from the end?" (two-pointer technique) or "How would you delete a node given only a reference to that node?" (copy data from next node).
Key Takeaways
- The second-to-last node is the real target, not the tail itself. Use
current.next.next != nullto find it. - Always handle empty and single-node lists before any traversal logic. These edge cases cause null pointer exceptions if ignored.
- Tail deletion in a singly linked list is inherently O(n) because forward-only traversal means you must walk the entire chain to find the penultimate node.
- This problem is a building block for harder linked list operations. The traversal pattern and pointer manipulation you practice here apply directly to problems like removing the Nth node from the end.
- In interviews, drawing the list and tracing through 2-3 nodes is the fastest way to catch off-by-one errors before they reach your code.
Practice and Related Problems
Once you're comfortable deleting the tail, push yourself with these related challenges:
- Insert a node at the end of a linked list (the inverse operation)
- Delete the Nth node from the end (uses the two-pointer technique)
- Reverse a linked list (builds on pointer manipulation skills)
- Find the middle of a linked list (another traversal pattern)
Consistent practice is what separates candidates who freeze at the whiteboard from those who solve problems confidently. This problem and hundreds of others are available on Firecode, where over 50,000 users have prepared for technical interviews and landed roles at top tech companies. Whether you're just starting with linked lists or polishing your skills before interview day, working through fundamentals like this will pay dividends.
Solutions in Other Languages
Python
class Solution:
def delete_tail(self, head):
if head is None or head.next is None:
return None
current = head
while current.next.next is not None:
current = current.next
current.next = None
return head
JavaScript
class Solution {
deleteTail(head) {
if (head === null || head.next === null) {
return null;
}
let current = head;
while (current.next.next !== null) {
current = current.next;
}
current.next = null;
return head;
}
}
TypeScript
class Solution {
deleteTail(head: ListNode | null): ListNode | null {
if (head === null || head.next === null) {
return null;
}
let current: ListNode = head;
while (current.next!.next !== null) {
current = current.next!;
}
current.next = null;
return head;
}
}
C++
class Solution {
public:
ListNode *deleteTail(ListNode *head) {
if (head == nullptr || head->next == nullptr) {
return nullptr;
}
ListNode *current = head;
while (current->next->next != nullptr) {
current = current->next;
}
current->next = nullptr;
return head;
}
};
Scala
class Solution {
def deleteTail(head: ListNode): ListNode = {
if (head == null || head.next == null) return null
var current = head
while (current.next.next != null) {
current = current.next
}
current.next = null
head
}
}
Kotlin
class Solution {
fun deleteTail(head: ListNode?): ListNode? {
if (head == null || head.next == null) {
return null
}
var current = head
while (current?.next?.next != null) {
current = current.next
}
current?.next = null
return head
}
}
Swift
class Solution {
func deleteTail(_ head: ListNode?) -> ListNode? {
if head == nil || head?.next == nil {
return nil
}
var current = head
while current?.next?.next != nil {
current = current?.next
}
current?.next = nil
return head
}
}
Ruby
class Solution
def delete_tail(head)
if head.nil? || head.next.nil?
return nil
end
current = head
while current.next.next != nil
current = current.next
end
current.next = nil
head
end
end
Dart
class Solution {
ListNode? deleteTail(ListNode? head) {
if (head == null || head.next == null) {
return null;
}
ListNode? current = head;
while (current?.next?.next != null) {
current = current?.next;
}
current?.next = null;
return head;
}
}
Rust
The Rust implementation uses Option<Box<ListNode>> for ownership and requires careful handling of mutable references:
impl Solution {
pub fn delete_tail(&self, head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
if head.is_none() || head.as_ref().unwrap().next.is_none() {
return None;
}
let mut head = head;
let mut current = &mut head;
while current.as_ref().unwrap().next.as_ref().unwrap().next.is_some() {
current = &mut current.as_mut().unwrap().next;
}
current.as_mut().unwrap().next = None;
head
}
}
PHP
class Solution {
public function deleteTail(?ListNode $head): ?ListNode {
if ($head === null || $head->next === null) {
return null;
}
$current = $head;
while ($current->next->next !== null) {
$current = $current->next;
}
$current->next = null;
return $head;
}
}
C#
public class Solution {
public ListNode? DeleteTail(ListNode? head) {
if (head == null || head.next == null) {
return null;
}
ListNode current = head;
while (current.next.next != null) {
current = current.next;
}
current.next = null;
return head;
}
}