Written guide
Singly Linked List: complete guide
A singly linked list stores values in nodes that each point to the next node, so inserting or deleting is just a matter of rewiring one or two pointers instead of shifting array elements. This lesson walks through insert, delete, and search, and the pointer-ordering habits that keep you from losing half your list.
Intuition
Think of a treasure hunt. Each clue card tells you something and also tells you where the next card is hidden. You cannot jump straight to clue number seven; you have to follow cards one through six. But if the organiser wants to add a new clue between cards three and four, nobody has to move any cards around. They just rewrite card three to point at the new card, and make the new card point at card four.
A singly linked list is that chain of cards. Each node holds a value and a next reference. The list itself only remembers the head, the first node. The last node's next is null. Compared with an array you give up instant indexing (no list[7] in O(1)), and in return you gain cheap insertion and deletion anywhere you already have a pointer, plus the ability to grow without ever resizing or copying.
How it works step by step
Every operation is a small variation on walking the chain with a cur (or prev) pointer and then changing at most two next fields.
- Insert at head: create a node, set
node.next = head, thenhead = node. O(1). - Insert after a node `prev`: set
node.next = prev.nextfirst, thenprev.next = node. Reversing the order makesprev.nextpoint at the new node before you saved the rest of the list, and the rest is lost. - Insert at index i: walk
i - 1steps from the head to findprev, then do the insert-after step. - Delete by value: walk with
prevuntilprev.nextholds the value, then bypass it withprev.next = prev.next.next. A dummy node in front of the head means deleting the head needs no special case. - Search: walk from the head, comparing each value, until you find it or reach
null.
Dry run
Start with the list 3 -> 7 -> 9 and apply five operations. The arrows show next pointers.
| Step | Operation | Pointer changes | List after |
|---|---|---|---|
| 0 | Start | head = node(3) | 3 -> 7 -> 9 -> null |
| 1 | insertAt(1, 5) | prev = node(3); new.next = node(7); node(3).next = new | 3 -> 5 -> 7 -> 9 -> null |
| 2 | remove(7) | prev walks dummy, 3, 5; node(5).next = node(9) | 3 -> 5 -> 9 -> null |
| 3 | addFirst(1) | new.next = node(3); head = new | 1 -> 3 -> 5 -> 9 -> null |
| 4 | indexOf(9) | visit 1, 3, 5, 9: found at index 3 | unchanged |
| 5 | remove(4) | prev reaches the last node, prev.next is null: return false | unchanged |
In step 2, node 7 is not destroyed explicitly. Nothing points to it any more, so in Java or Python the garbage collector reclaims it. In C or C++ you would have to free it yourself, after saving any pointer you still need.
Code
public class SinglyLinkedList {
private static class Node {
int val;
Node next;
Node(int val) { this.val = val; }
}
private Node head;
private int size;
public void addFirst(int val) {
Node n = new Node(val);
n.next = head;
head = n;
size++;
}
public void addLast(int val) {
Node n = new Node(val);
if (head == null) {
head = n;
} else {
Node cur = head;
while (cur.next != null) cur = cur.next;
cur.next = n;
}
size++;
}
/** Inserts val so that it ends up at position index (0-based). */
public void insertAt(int index, int val) {
if (index < 0 || index > size) throw new IndexOutOfBoundsException("index " + index);
if (index == 0) { addFirst(val); return; }
Node prev = head;
for (int i = 0; i < index - 1; i++) prev = prev.next;
Node n = new Node(val);
n.next = prev.next; // 1. new node grabs the rest of the list
prev.next = n; // 2. predecessor points at the new node
size++;
}
/** Removes the first node holding val. Returns true if one was removed. */
public boolean remove(int val) {
Node dummy = new Node(0);
dummy.next = head;
Node prev = dummy;
while (prev.next != null && prev.next.val != val) prev = prev.next;
if (prev.next == null) return false;
prev.next = prev.next.next; // bypass the node
head = dummy.next; // head changes if we removed the first node
size--;
return true;
}
public int indexOf(int val) {
int i = 0;
for (Node cur = head; cur != null; cur = cur.next, i++) {
if (cur.val == val) return i;
}
return -1;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Node cur = head; cur != null; cur = cur.next) sb.append(cur.val).append(" -> ");
return sb.append("null").toString();
}
public static void main(String[] args) {
SinglyLinkedList list = new SinglyLinkedList();
list.addLast(3); list.addLast(7); list.addLast(9);
list.insertAt(1, 5);
list.remove(7);
list.addFirst(1);
System.out.println(list + " indexOf(9) = " + list.indexOf(9));
}
}class Node:
def __init__(self, val, next=None):
self.val = val
self.next = next
class SinglyLinkedList:
def __init__(self):
self.head = None
self.size = 0
def add_first(self, val):
self.head = Node(val, self.head)
self.size += 1
def add_last(self, val):
node = Node(val)
if self.head is None:
self.head = node
else:
cur = self.head
while cur.next:
cur = cur.next
cur.next = node
self.size += 1
def insert_at(self, index, val):
if not 0 <= index <= self.size:
raise IndexError(index)
if index == 0:
self.add_first(val)
return
prev = self.head
for _ in range(index - 1):
prev = prev.next
prev.next = Node(val, prev.next) # new node takes prev.next first
self.size += 1
def remove(self, val):
dummy = Node(0, self.head)
prev = dummy
while prev.next and prev.next.val != val:
prev = prev.next
if prev.next is None:
return False
prev.next = prev.next.next
self.head = dummy.next
self.size -= 1
return True
def index_of(self, val):
for i, v in enumerate(self):
if v == val:
return i
return -1
def __iter__(self):
cur = self.head
while cur:
yield cur.val
cur = cur.next
lst = SinglyLinkedList()
for x in (3, 7, 9):
lst.add_last(x)
lst.insert_at(1, 5)
lst.remove(7)
lst.add_first(1)
print(list(lst), lst.index_of(9)) # [1, 3, 5, 9] 3Time and space complexity
| Operation | Best | Average | Worst | Extra space |
|---|---|---|---|---|
| Insert at head | O(1) | O(1) | O(1) | O(1) |
| Insert at tail (no tail pointer) | O(n) | O(n) | O(n) | O(1) |
| Insert at index i | O(1) at i = 0 | O(i) | O(n) | O(1) |
| Delete by value | O(1) at head | O(n) | O(n) | O(1) |
| Search | O(1) | O(n) | O(n) | O(1) |
| Storage for n items | - | - | - | O(n), one pointer per node |
The rewiring itself is always constant time; what costs O(n) is finding the spot, because the only way to reach node i is to follow i pointers. Keeping a tail reference makes appending O(1), which is how linked-list queues work. Deleting the tail stays O(n), though, since you still need the node before it.
Edge cases
- Empty list:
headisnull; search returns -1, delete returns false, insert at 0 becomes the head. - Deleting the head: handled by the dummy node, or by an explicit
head = head.nextbranch. - Deleting the only node: the list must end with
head = null(andtail = nullif you keep one). - Insert at index
size: equivalent to appending at the tail. - Index out of range: validate before walking, or you will dereference
nullmid-walk. - Duplicate values: decide whether
removedeletes the first match or all of them.
Common mistakes
- Losing the next pointer. Writing
prev.next = nodebeforenode.next = prev.nextleaves the new node pointing at itself and orphans everything after it. Always save the old link first. - Dereferencing null. Loops like
while (cur.next.val != val)crash at the end of the list. Checkcur.next != nullfirst. - Moving the head while traversing. Using
head = head.nextto walk the list destroys it. Walk with a separatecur. - Forgetting to update `head` after deleting or inserting at position 0.
- Not updating `tail` and `size` in implementations that track them; they silently drift out of sync.
- Walking one step too far. To insert at index i you need the node at i - 1, not at i.
Variations and when to use it
A tail pointer makes the list a perfect O(1) queue. A circular list links the last node back to the head, useful for round-robin scheduling. A doubly linked list adds a prev pointer so you can delete any node in O(1) and walk backwards, at the cost of extra memory per node. Many classic interview tricks are built on singly linked lists: reversing with three pointers, finding the middle with slow and fast pointers, cycle detection with Floyd's algorithm, and merging two sorted lists.
Use a singly linked list when you mostly insert or remove at the front, when you need a stack or queue that never resizes, or when an interview problem simply hands you a ListNode. Prefer an array or ArrayList when you need random access or care about cache performance; contiguous memory is much faster to scan than nodes scattered across the heap.
Related LeetCode problems
- #206 Reverse Linked List — rewire each
nextbackwards while saving the old next first. - #328 Odd Even Linked List — maintain two chains and splice the even chain after the odd one.
- #2095 Delete the Middle Node of a Linked List — slow and fast pointers find the node before the middle, then bypass.
- #2130 Maximum Twin Sum of a Linked List — find the middle, reverse the second half, then walk both halves together.
- #203 Remove Linked List Elements — the dummy-node delete loop, removing every match.
- #19 Remove Nth Node From End of List — a gap of n between two pointers locates the predecessor in one pass.
- #21 Merge Two Sorted Lists — splice nodes from two lists onto a dummy tail.
- #141 Linked List Cycle — fast pointer moves two steps, slow moves one; they meet only if there is a cycle.
FAQ
›When is a linked list better than an array?
When you insert and delete frequently at positions you already hold a pointer to, especially at the front, and rarely need random access. Arrays win for indexing, binary search, and fast sequential scans thanks to cache locality.
›What is a dummy (sentinel) node and why use it?
It is a placeholder node placed before the real head. Because every real node then has a predecessor, deleting or inserting at the head uses the same code as anywhere else, which removes a whole class of edge-case bugs.
›Why is accessing the k-th element O(n)?
Nodes are scattered in memory and only connected through next pointers, so the only way to reach the k-th node is to follow k pointers from the head.
›How do you delete a node when you only have a pointer to it?
In a singly linked list you cannot reach its predecessor, so the trick is to copy the next node's value into it and then delete the next node instead. This fails for the tail node, which is why doubly linked lists exist.
›Does Java's LinkedList use a singly or doubly linked list?
java.util.LinkedList is doubly linked, which lets it act as both a list and a deque with O(1) operations at either end.
