Insert a node at the end of a linked list
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
- Empty list (null head): The new node becomes the head
- Single node: Traverse one step, link the new node
- Negative values: The algorithm handles any integer
- 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:
- Create the new node with the given value
- Handle the empty list by returning the new node as the head
- 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
newNodeis created first, before any branching logic. Both the empty-list and non-empty-list paths need it. - The
getTailNodehelper uses a separateiteratorvariable. If you usedheaddirectly in the while loop, you'd lose your reference to the beginning of the list. - Returning
headat the end is safe because the head never changes for non-empty lists. Only the tail'snextpointer 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
-
Forgetting the null-head check: Without it, calling
getTailNode(null)causes a NullPointerException on the firstiterator.nextaccess. -
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. -
Off-by-one in the loop condition: Using
iterator != nullinstead ofiterator.next != nullmeans 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:
-
Clarify edge cases upfront: "What should I return for an empty list?" This shows awareness before you start coding.
-
Draw the list: Sketch a 3-node list and show where the new node attaches. Visual communication helps avoid misunderstandings.
-
Mention the tail-pointer optimization: After solving, note that maintaining a tail pointer makes append O(1). This shows you think about design tradeoffs.
-
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 != nullstops 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;
}
}