Lesson 30 of 164
Insertion at the Tail of a Linked List
Given the head of a singly linked list and a value, add a new node with that value at the end of the list, and return the head.
Example 1
Input: 1 -> 2 -> 3, value = 4
Output: 1 -> 2 -> 3 -> 4graph LR
A["1"] --> B["2"] --> C["3"] --> D["4 (new)"] --> N(["null"])
style D stroke-width:3pxExample 2
Input: empty list, value = 7
Output: 7Insertion at the head, for comparison
Adding at the front is O(1): the new node's next becomes the old head.
import java.util.ArrayList;
import java.util.List;
public class Main {
static class ListNode {
int val;
ListNode next;
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static ListNode fromArray(int... values) {
ListNode head = null;
for (int i = values.length - 1; i >= 0; i--) {
head = new ListNode(values[i], head);
}
return head;
}
static List<Integer> toList(ListNode list) {
List<Integer> values = new ArrayList<>();
for (ListNode node = list; node != null; node = node.next) {
values.add(node.val);
}
return values;
}
static ListNode insertAtHead(ListNode head, int val) {
return new ListNode(val, head);
}
public static void main(String[] args) {
System.out.println(toList(insertAtHead(fromArray(1, 2, 3), 0)));
}
}[0, 1, 2, 3]The tail is harder, because a singly linked list only knows its head, so we have to walk to the end first. The nodes here are objects with a mutable next field, and null means "no next node", so no wrapper types are needed.
Brute force: recursion
Think about the list as "a head node, followed by a smaller list":
- If the list is empty (
null), the result is the new node. - Otherwise, insert into the rest of the list (
head.next), reattach the result, and return the node.
import java.util.ArrayList;
import java.util.List;
public class Main {
static class ListNode {
int val;
ListNode next;
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static ListNode fromArray(int... values) {
ListNode head = null;
for (int i = values.length - 1; i >= 0; i--) {
head = new ListNode(values[i], head);
}
return head;
}
static List<Integer> toList(ListNode list) {
List<Integer> values = new ArrayList<>();
for (ListNode node = list; node != null; node = node.next) {
values.add(node.val);
}
return values;
}
static ListNode insertAtTail(ListNode head, int val) {
if (head == null) return new ListNode(val, null);
head.next = insertAtTail(head.next, val);
return head;
}
public static void main(String[] args) {
System.out.println(toList(insertAtTail(fromArray(1, 2, 3), 4)));
System.out.println(toList(insertAtTail(null, 7)));
}
}[1, 2, 3, 4]
[7]Time: O(n). Space: O(n) for the call stack, with one frame per node. A list with a few hundred thousand nodes could throw StackOverflowError.
Optimal: walk to the last node
cur starts at head, and each step moves it to cur.next. When cur.next is null, cur is the last node, and we assign the new node to cur.next.
Walkthrough with 1 -> 2 -> 3, value 4:
| step | cur |
cur.next |
action |
|---|---|---|---|
| 1 | node 1 | node 2 | move cur forward |
| 2 | node 2 | node 3 | move cur forward |
| 3 | node 3 | null |
set cur.next to the new node |
import java.util.ArrayList;
import java.util.List;
public class Main {
static class ListNode {
int val;
ListNode next;
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static ListNode fromArray(int... values) {
ListNode head = null;
for (int i = values.length - 1; i >= 0; i--) {
head = new ListNode(values[i], head);
}
return head;
}
static List<Integer> toList(ListNode list) {
List<Integer> values = new ArrayList<>();
for (ListNode node = list; node != null; node = node.next) {
values.add(node.val);
}
return values;
}
static ListNode insertAtTail(ListNode head, int val) {
ListNode node = new ListNode(val, null);
if (head == null) return node; // empty list: the new node is the head
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
return head;
}
public static void main(String[] args) {
System.out.println(toList(insertAtTail(fromArray(1, 2, 3), 4)));
System.out.println(toList(insertAtTail(null, 7)));
}
}[1, 2, 3, 4]
[7]Time: O(n). Space: O(1).
The empty list needs a special case: there is no last node to attach to, so the new node becomes the head. A dummy node placed before the head, as in the last example below, removes that case.
Appending many values: keep a tail reference
Calling insertAtTail n times walks the list n times, which is O(n²) in total. When building a list, keep a reference to the last node instead, so each append is O(1):
import java.util.ArrayList;
import java.util.List;
public class Main {
static class ListNode {
int val;
ListNode next;
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static ListNode fromArray(int... values) {
ListNode head = null;
for (int i = values.length - 1; i >= 0; i--) {
head = new ListNode(values[i], head);
}
return head;
}
static List<Integer> toList(ListNode list) {
List<Integer> values = new ArrayList<>();
for (ListNode node = list; node != null; node = node.next) {
values.add(node.val);
}
return values;
}
static ListNode build(int[] values) {
ListNode dummy = new ListNode(0, null);
ListNode tail = dummy;
for (int val : values) {
tail.next = new ListNode(val, null);
tail = tail.next;
}
return dummy.next;
}
public static void main(String[] args) {
System.out.println(toList(build(new int[]{1, 2, 3, 4})));
}
}[1, 2, 3, 4]A list class can also store a permanent tail field for O(1) appends. Two references to the same node are allowed in Java, so the tail field and the previous node's next can both point at the last node. The class must then update tail on every append and on every removal of the last node.
Edge cases
- Empty list: the new node becomes the head.
- One-node list: the loop runs once.
