Insert a node at the end of a linked list

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

You're in a phone screen for Oracle and the interviewer asks you to append a value to the end of a singly linked list. It sounds straightforward, but this problem is a litmus test for how well you understand pointer manipulation and edge cases. Get the null-head case wrong or accidentally lose your head reference during traversal, and the interview goes sideways fast. This problem is also commonly known as "Append Node to Linked List" on other platforms.

TL;DR

Create a new node, handle the empty-list edge case by returning it directly, then traverse to the tail and link the new node. This runs in O(n) time and O(1) space. The two things that trip people up: forgetting the null-head check, and overwriting the head pointer during traversal instead of using a separate iterator.

Why This Problem Matters

Inserting at the end of a linked list is one of the first linked list operations you should master. It tests your ability to traverse a list, handle null pointers, and maintain references correctly. These same skills transfer directly to harder linked list problems like reversal, cycle detection, and merging sorted lists.

Linked Lists: A Quick Refresher

A singly linked list is a chain of nodes where each node stores a value and a pointer to the next node. The last node points to null, marking the end of the list.

Loading visualization...

Each ListNode has two properties:

  • data: The integer value stored in the node
  • next: A reference to the next node, or null if this is the last node

You can only move forward through the list. There's no way to jump to the end directly, which is why appending requires a full traversal.

Understanding the Problem

Given: The head of a singly linked list and an integer value Task: Create a new node with that value and attach it to the end of the list Return: The head of the modified list

Here's what the transformation looks like for a list [1, 2] with value 3:

Loading visualization...

After inserting 3:

Loading visualization...

Prefer a different language? Jump to solutions in other languages.

Edge Cases to Consider

  1. Empty list (null head): The new node becomes the head
  2. Single node: Traverse one step, link the new node
  3. Negative values: The algorithm handles any integer
  4. Large lists: Linear traversal is unavoidable without a tail pointer

The empty list case deserves special attention. When head is null, there's no list to traverse, so the new node is both the head and the tail:

Loading visualization...

After inserting 10 into a single-node list:

Loading visualization...

Solution Approach

The algorithm has three parts:

  1. Create the new node with the given value
  2. Handle the empty list by returning the new node as the head
  3. Traverse to the tail and link the new node

For traversal, you need to find the node whose next is null. That's the tail. Once found, set tail.next to the new node.

Walking Through the Traversal

Starting with [1, 2, 3, 4, 5] and inserting 6:

Step 1: Start at the head (node 1). Its next is not null, so keep going.

Loading visualization...

Step 2: Move to node 2. Still not the tail.

Loading visualization...

This continues until we reach node 5, where next is null.

Found the tail: Node 5 is the last node.

Loading visualization...

Link the new node: Set node5.next = newNode(6).

Loading visualization...

The Firecode solution extracts the traversal into a helper method called getTailNode, which keeps the main method clean and focused on the insertion logic.

Implementation

Here's the Java solution:

public class Solution {
  public ListNode insertAtEnd(ListNode head, int n) {
    // Create the new node to insert
    ListNode newNode = new ListNode(n);

    // If the list is empty, the new node becomes the head
    if (head == null) {
      return newNode;
    }

    // Find the last node and link the new node
    ListNode tailNode = getTailNode(head);
    tailNode.next = newNode;

    // Return the original head (unchanged)
    return head;
  }

  private ListNode getTailNode(ListNode head) {
    ListNode iterator = head;
    while (iterator.next != null) {
      iterator = iterator.next;
    }
    return iterator;
  }
}

A few things to note about this implementation:

  • The newNode is created first, before any branching logic. Both the empty-list and non-empty-list paths need it.
  • The getTailNode helper uses a separate iterator variable. If you used head directly in the while loop, you'd lose your reference to the beginning of the list.
  • Returning head at the end is safe because the head never changes for non-empty lists. Only the tail's next pointer is modified.

Why a Helper Method?

Extracting the traversal into getTailNode follows the single-responsibility principle. The main method handles insertion logic, while the helper handles traversal. In an interview, this kind of decomposition signals clean thinking.

You could also inline the traversal, but the separated version is easier to test and reason about.

Complexity Analysis

Time Complexity: O(n)

You traverse every node exactly once to find the tail. Creating the new node and linking it are both O(1) operations. The traversal dominates, giving O(n) overall.

If you had a tail pointer, insertion would be O(1). But this problem gives you only the head, so linear traversal is the best you can do.

Space Complexity: O(1)

You allocate exactly one new node and use a constant number of pointer variables (newNode, tailNode, iterator). No auxiliary data structures are needed.

Common Pitfalls

  1. Forgetting the null-head check: Without it, calling getTailNode(null) causes a NullPointerException on the first iterator.next access.

  2. Overwriting head during traversal: If you write while (head.next != null) { head = head.next; }, you've lost your reference to the beginning of the list. Always use a separate variable.

  3. Off-by-one in the loop condition: Using iterator != null instead of iterator.next != null means you overshoot past the tail and can't link the new node.

// Wrong - iterator ends up null, can't set .next
while (iterator != null) {
  iterator = iterator.next;
}
// iterator is now null - no way to link newNode

// Correct - iterator stops at the tail node
while (iterator.next != null) {
  iterator = iterator.next;
}
// iterator.next is null, so iterator IS the tail
iterator.next = newNode;

Interview Tips

When solving this problem in an interview:

  1. Clarify edge cases upfront: "What should I return for an empty list?" This shows awareness before you start coding.

  2. Draw the list: Sketch a 3-node list and show where the new node attaches. Visual communication helps avoid misunderstandings.

  3. Mention the tail-pointer optimization: After solving, note that maintaining a tail pointer makes append O(1). This shows you think about design tradeoffs.

  4. Consider follow-ups: The interviewer might ask you to insert at the beginning (O(1)), at a specific index, or to implement a doubly linked list version.

Handling Negative Values

The algorithm works identically for negative numbers and zero. Here's [-5, -3, -1] after inserting 0:

Loading visualization...

The data type stored in nodes doesn't affect the traversal or linking logic at all.

Key Takeaways

  • Always handle the null-head edge case first. When the list is empty, the new node is the head.
  • Use a separate iterator variable for traversal. Never modify the head pointer while traversing.
  • The loop condition iterator.next != null stops at the tail node, not past it. This is critical for linking the new node.
  • Extracting traversal into a helper method keeps the insertion logic clean and testable.
  • With only a head pointer, O(n) traversal is unavoidable. Mention the O(1) tail-pointer optimization in interviews to demonstrate design awareness.

Related Problems

Once you're comfortable appending to a linked list, try these to build on the same skills:

  • Insert a node at the beginning of a linked list
  • Remove duplicate nodes from a linked list
  • Reverse a linked list
  • Find the middle of a linked list

These problems all require pointer manipulation and careful null handling, the same fundamentals tested here.

Consistent practice is the key to succeeding in coding interviews. This problem and thousands of others are available on Firecode, where over 50,000 users have successfully prepared for technical interviews and landed six and seven-figure jobs at top tech companies. Whether you're just starting out or brushing up before an interview, mastering linked list fundamentals like this will pay dividends.

Solutions in Other Languages

Python

class Solution:
    def insert_at_end(self, head, n):
        new_node = ListNode(n)
        if head is None:
            return new_node
        tail_node = self.get_tail_node(head)
        tail_node.next = new_node
        return head

    @staticmethod
    def get_tail_node(head):
        iterator = head
        while iterator.next is not None:
            iterator = iterator.next
        return iterator

JavaScript

class Solution {
  insertAtEnd(head, n) {
    const newNode = new ListNode(n);
    if (head === null) {
      return newNode;
    }
    const tailNode = this.getTailNode(head);
    tailNode.next = newNode;
    return head;
  }

  getTailNode(head) {
    let iterator = head;
    while (iterator.next !== null) {
      iterator = iterator.next;
    }
    return iterator;
  }
}

TypeScript

class Solution {
  insertAtEnd(head: ListNode | null, n: number): ListNode | null {
    const newNode = new ListNode(n);
    if (head === null) {
      return newNode;
    }
    const tailNode = this.getTailNode(head);
    tailNode.next = newNode;
    return head;
  }

  private getTailNode(head: ListNode): ListNode {
    let iterator: ListNode = head;
    while (iterator.next !== null) {
      iterator = iterator.next;
    }
    return iterator;
  }
}

C++

class Solution {
public:
  ListNode *insertAtEnd(ListNode *head, int n) {
    auto *newNode = new ListNode(n);
    if (head == nullptr) {
      return newNode;
    }
    ListNode *tailNode = getTailNode(head);
    tailNode->next = newNode;
    return head;
  }

private:
  ListNode *getTailNode(ListNode *head) {
    ListNode *iterator = head;
    while (iterator->next != nullptr) {
      iterator = iterator->next;
    }
    return iterator;
  }
};

Go

func (s *Solution) InsertAtEnd(head *ListNode, n int) *ListNode {
    newNode := &ListNode{Data: n, Next: nil}
    if head == nil {
        return newNode
    }
    tailNode := getTailNode(head)
    tailNode.Next = newNode
    return head
}

func getTailNode(head *ListNode) *ListNode {
    iterator := head
    for iterator.Next != nil {
        iterator = iterator.Next
    }
    return iterator
}

Scala

class Solution {
  def insertAtEnd(head: ListNode, n: Int): ListNode = {
    val newNode = new ListNode(n)
    if (head == null) return newNode
    val tailNode = getTailNode(head)
    tailNode.next = newNode
    head
  }

  private def getTailNode(head: ListNode) = {
    var iterator = head
    while (iterator.next != null) {
      iterator = iterator.next
    }
    iterator
  }
}

Kotlin

class Solution {
  fun insertAtEnd(head: ListNode?, n: Int): ListNode? {
    val newNode = ListNode(n)
    if (head == null) {
      return newNode
    }
    val tailNode = getTailNode(head)
    tailNode.next = newNode
    return head
  }

  private fun getTailNode(head: ListNode): ListNode {
    var iterator = head
    while (iterator.next != null) {
      iterator = iterator.next!!
    }
    return iterator
  }
}

Swift

class Solution {
    func insertAtEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
        let newNode = ListNode(n)
        if head == nil {
            return newNode
        }
        let tailNode = getTailNode(head!)
        tailNode.next = newNode
        return head
    }

    private func getTailNode(_ head: ListNode) -> ListNode {
        var iterator = head
        while iterator.next != nil {
            iterator = iterator.next!
        }
        return iterator
    }
}

Rust

Rust's ownership model makes linked list operations more involved. The solution uses Option<Box<ListNode>> and mutable references to traverse and append.

impl Solution {
    pub fn insert_at_end(&self, head: Option<Box<ListNode>>, n: i32) -> Option<Box<ListNode>> {
        let new_node = Some(Box::new(ListNode::new(n)));
        if head.is_none() {
            return new_node;
        }
        let mut head = head;
        let mut current = &mut head;
        while current.as_ref().unwrap().next.is_some() {
            current = &mut current.as_mut().unwrap().next;
        }
        current.as_mut().unwrap().next = new_node;
        head
    }
}

Ruby

class Solution
  def insert_at_end(head, n)
    new_node = ListNode.new(n)
    return new_node if head.nil?
    tail_node = get_tail_node(head)
    tail_node.next = new_node
    head
  end

  private def get_tail_node(head)
    iterator = head
    while iterator.next
      iterator = iterator.next
    end
    iterator
  end
end

Dart

class Solution {
  ListNode? insertAtEnd(ListNode? head, int n) {
    ListNode newNode = ListNode(n);
    if (head == null) {
      return newNode;
    }
    ListNode tailNode = _getTailNode(head);
    tailNode.next = newNode;
    return head;
  }

  ListNode _getTailNode(ListNode head) {
    ListNode iterator = head;
    while (iterator.next != null) {
      iterator = iterator.next!;
    }
    return iterator;
  }
}

PHP

class Solution {
    public function insertAtEnd(?ListNode $head, int $n): ListNode {
        $newNode = new ListNode($n);
        if ($head === null) {
            return $newNode;
        }
        $tailNode = $this->getTailNode($head);
        $tailNode->next = $newNode;
        return $head;
    }

    private function getTailNode(ListNode $head): ListNode {
        $iterator = $head;
        while ($iterator->next !== null) {
            $iterator = $iterator->next;
        }
        return $iterator;
    }
}

C#

public class Solution {
    public ListNode? insertAtEnd(ListNode? head, int n) {
        ListNode newNode = new ListNode(n);
        if (head == null) {
            return newNode;
        }
        ListNode tailNode = GetTailNode(head);
        tailNode.next = newNode;
        return head;
    }

    private ListNode GetTailNode(ListNode head) {
        ListNode iterator = head;
        while (iterator.next != null) {
            iterator = iterator.next;
        }
        return iterator;
    }
}

Frequently Asked Questions

What is the time complexity of inserting a node at the end of a linked list?
The time complexity is O(n) where n is the number of nodes in the list. You must traverse the entire list to find the last node before appending the new node. If you maintain a tail pointer, insertion becomes O(1), but this problem assumes you only have access to the head.
What is the space complexity of appending to a linked list?
The space complexity is O(1) because you only allocate one new node and use a constant number of pointer variables for traversal, regardless of the list size.
How do you handle inserting into an empty linked list?
When the head is null, the new node becomes both the head and the tail of the list. Simply create the new node and return it as the head. This is the most important edge case to handle correctly.
What is the difference between inserting at the beginning versus the end of a linked list?
Inserting at the beginning is O(1) because you just point the new node's next to the current head and return the new node. Inserting at the end is O(n) because you must traverse to the tail first. If performance matters and you frequently append, consider maintaining a tail pointer.
Why do you return the head after inserting at the end?
Returning the head preserves the reference to the beginning of the list for the caller. The head only changes when inserting into an empty list, where the new node becomes the head. For non-empty lists, the head remains the same, but returning it provides a consistent API.
Can you insert at the end of a linked list without traversing it?
Not with just a head pointer. You need to reach the last node to update its next reference. However, if you maintain a separate tail pointer, you can append in O(1) by directly updating tail.next and then updating the tail pointer.
How does this problem differ from inserting at a specific position?
Inserting at the end always appends after the last node, requiring a full traversal. Inserting at position k requires traversing only k nodes to find the insertion point. Both share the same edge case handling for empty lists, but positional insertion also needs bounds checking.
What are common mistakes when inserting at the end of a linked list?
The most common mistakes are forgetting to handle the empty list case (null head), losing the head reference by reassigning it during traversal, and creating infinite loops by incorrectly linking the new node. Always use a separate iterator variable for traversal.