Delete the tail node of a linked list

AS
Ackshaey Singh
Difficulty Easy
Time Complexity
O(n)
Space Complexity
O(1)
Linked list
Firecode

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:

  1. Empty list (null head): Nothing to delete, return null
  2. Single node: The head is also the tail, so deleting the tail means returning null
  3. 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:

  1. If the list is empty or has one node, return null
  2. Start a pointer current at the head
  3. Advance current until current.next.next is null
  4. Set current.next = null to remove the tail
  5. 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]:

  1. head is node 1, head.next is node 2 - neither is null, so we skip the guard clause
  2. current starts at node 1
  3. current.next.next is null (node 2 is the tail) - the while loop doesn't execute
  4. current.next = null removes node 2
  5. 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

  1. Using current.next != null as 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.
    
  2. Forgetting the single-node edge case: If you only check head == null, a single-node list will crash when you access head.next.next.

  3. Not returning the original head: After deleting the tail, the head hasn't changed. Make sure you return head, not current.

  4. Null pointer on an empty list: Accessing head.next when head is null throws an exception. Always guard against null first.

Interview Tips

When you encounter this problem in an interview:

  1. Start with edge cases: Tell the interviewer "I'll handle the empty list and single-node cases first." This shows disciplined thinking.

  2. Draw the list: Sketch 3-4 nodes on the whiteboard. Circle the second-to-last node and explain why it's the key.

  3. Explain the loop condition: Say "I'm checking current.next.next because I need to stop one node before the tail." This is the core insight, and articulating it clearly demonstrates understanding.

  4. 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.

  5. 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 != null to 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;
    }
}

Frequently Asked Questions

What is the time complexity of deleting the tail node of a linked list?
The time complexity is O(n) where n is the number of nodes, because you must traverse the entire list to find the second-to-last node. Unlike doubly linked lists where the tail can be removed in O(1) with a prev pointer, singly linked lists require a full traversal since there is no way to go backward from the tail.
Why can't you delete the tail node in O(1) time for a singly linked list?
To remove the tail, you need to set the second-to-last node's next pointer to null. In a singly linked list, you can only traverse forward, so you must walk from the head all the way to the second-to-last node. A doubly linked list supports O(1) tail deletion because each node has a prev pointer.
What should you return when the linked list is empty or has only one node?
Return null in both cases. An empty list (null head) has no nodes to delete. A single-node list has its only node as both head and tail, so deleting the tail means removing the only node, leaving an empty list.
How do you find the second-to-last node in a singly linked list?
Traverse the list with a pointer and check current.next.next at each step. When current.next.next is null, the current node is the second-to-last node. Then set current.next to null to remove the tail.
What is the difference between deleting the head and deleting the tail of a linked list?
Deleting the head is O(1) because you simply return head.next. Deleting the tail is O(n) because you must traverse to the second-to-last node to sever the link. This asymmetry is a fundamental characteristic of singly linked lists and a common interview talking point.
Can you delete the tail node without traversing the entire list?
Not with a singly linked list. Even with a tail pointer, you still need the predecessor node to update its next pointer to null, which requires traversal. O(1) tail deletion requires a doubly linked list, where each node has a prev pointer that gives direct access to the predecessor.
How does this problem relate to other linked list problems in coding interviews?
Tail deletion is a foundational operation that tests pointer traversal and edge case handling. It builds toward harder problems like removing the Nth node from the end (which uses the two-pointer technique), reversing a linked list, and implementing LRU caches where both head and tail operations matter.
Why is the while loop condition current.next.next != null instead of current.next != null?
Using current.next != null would stop at the tail itself, but you need the node before the tail to sever the link. By checking current.next.next != null, you stop at the second-to-last node, which is exactly where you need to set next to null.