Use arrow keys or buttons to navigate • Press S for step reveals
1 / 20
What is a Linked List?
A linked list is a linear data structure where each element (called a node) contains data and a reference (pointer) to the next node in the sequence.
Each node has two parts:
+--------+--------+ +--------+--------+ +--------+--------+
| data | next |----->| data | next |----->| data | next |----> null
+--------+--------+ +--------+--------+ +--------+--------+
"data" = the value stored "next" = pointer to the next node
Analogy: Treasure Hunt
Imagine a treasure hunt where each clue tells you where to find the next clue. You can only follow the chain forward -- you cannot jump directly to clue #5. Each clue (node) has content (data) and directions to the next clue (pointer).
Analogy: Train Cars
Think of a linked list as a train. Each car (node) carries cargo (data) and is coupled to the next car (next pointer). The engine is the head. The last car has no coupling -- it points to null.
Key Idea
Unlike arrays, linked list nodes do NOT need to be stored contiguously in memory. They can be scattered anywhere -- the pointers connect them into a logical sequence.
* Amortized O(1) for dynamic arrays (ArrayList). Worst case O(n) when resizing.
3 / 20
The Node Class
Every linked list is built from Node objects. Each node stores one piece of data and one reference to the next node.
Java Implementation
public class Node {
int data; // the value stored
Node next; // pointer to next node
// Constructor
public Node(int data) {
this.data = data;
this.next = null;
}
}
The next field is itself a Node reference -- this is what creates the chain. When next is null, we have reached the end of the list.
Anatomy of a Single Node
Node myNode = new Node(42);
myNode
|
v
+----------+-----------+
| data: 42 | next: null|
+----------+-----------+
The node exists in memory,
holding value 42 and pointing
to nothing (null).
Key Idea
A Node is a self-referential structure: it contains a reference to another object of its own type. This is the fundamental building block for all linked structures (lists, trees, graphs).
4 / 20
Singly Linked List Structure
A singly linked list has three key properties: a head pointer, one-way traversal, and a null terminator.
head
|
v
+------+---+ +------+---+ +------+---+ +------+------+
| 10 | *-+--->| 20 | *-+--->| 30 | *-+--->| 40 | null |
+------+---+ +------+---+ +------+---+ +------+------+
Traversal direction: ============================>
(one way only -- you CANNOT go backwards)
Java Wrapper Class
public class SinglyLinkedList {
private Node head; // entry point
private int size; // # of elements
public SinglyLinkedList() {
this.head = null;
this.size = 0;
}
}
Three Things to Remember
head -- the only entry point into the list. Lose it and you lose the entire list.
Singly linked -- each node only knows about the next node, never the previous one.
null terminator -- the last node's next is null, signaling the end.
5 / 20
Creating a Linked List (Step by Step)
Let's build the list 10 -> 20 -> 30 from scratch.
Step 1: Create first node, set head
Node a = new Node(10);
head = a;
head
|
v
+------+------+
| 10 | null |
+------+------+
Step 2: Create second node, link it
Node b = new Node(20);
a.next = b;
head
|
v
+------+---+ +------+------+
| 10 | *-+--->| 20 | null |
+------+---+ +------+------+
Step 3: Create third node, link it
Node c = new Node(30);
b.next = c;
head
|
v
+------+---+ +------+---+ +------+------+
| 10 | *-+--->| 20 | *-+--->| 30 | null |
+------+---+ +------+---+ +------+------+
Warning: Never Lose the Head!
If you accidentally reassign head without saving the old reference, all nodes become unreachable and are lost (garbage collected in Java, leaked in C/C++).
6 / 20
Traversal
Walking through every node in the list using a temporary current pointer.
Java Code
public void printList() {
Node current = head;
while (current != null) {
System.out.print(current.data + " -> ");
current = current.next;
}
System.out.println("null");
}
Key Idea
We use a temporary pointer (current) so we don't modify head. Moving head would lose access to the beginning of the list!
Step-by-Step Visualization
Iteration 1: current = node(10)
[10|*]--->[20|*]--->[30|null]
^cur
Iteration 2: current = node(20)
[10|*]--->[20|*]--->[30|null]
^cur
Iteration 3: current = node(30)
[10|*]--->[20|*]--->[30|null]
^cur
Iteration 4: current = null
STOP. (while condition is false)
Output: 10 -> 20 -> 30 -> null
7 / 20
Insert at Head -- O(1)
The fastest insertion: add a new node at the front of the list.
BEFORE: Insert 5 at head
head
|
v
+------+---+ +------+---+ +------+------+
| 10 | *-+--->| 20 | *-+--->| 30 | null |
+------+---+ +------+---+ +------+------+
Step 1: Create new node Step 2: Point new node to old head
newNode = new Node(5); newNode.next = head;
+------+------+ head +------+---+ head
| 5 | null | | | 5 | *-+--+ |
+------+------+ v +------+---+ | v
[10|*]-->[20|*]-->[30|null] +->[10|*]-->[20|*]-->[30|null]
Step 3: Move head to new node
head = newNode;
head
|
v
+------+---+ +------+---+ +------+---+ +------+------+
| 5 | *-+--->| 10 | *-+--->| 20 | *-+--->| 30 | null |
+------+---+ +------+---+ +------+---+ +------+------+
public void insertAtHead(int data) {
Node newNode = new Node(data);
newNode.next = head; // Step 2
head = newNode; // Step 3
size++;
}
Warning: Order Matters!
WRONG:head = newNode; then newNode.next = head; -- this creates a self-loop! The new node points to itself and the rest of the list is lost forever.
RIGHT: Always link first (newNode.next = head), then move head.
8 / 20
Insert at Tail -- O(n)
We must traverse the entire list to find the last node before we can append.
BEFORE: Insert 40 at tail
head
|
v
[10|*]--->[20|*]--->[30|null]
Step 1: Create new node
newNode = new Node(40); +------+------+
| 40 | null |
+------+------+
Step 2: Traverse to find last node
current = head;
while (current.next != null)
current = current.next;
current is now at node(30)
Step 3: Link last node to new node
current.next = newNode;
AFTER:
head
|
v
[10|*]--->[20|*]--->[30|*]--->[40|null]
public void insertAtTail(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode; // empty list
} else {
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
size++;
}
Why O(n)?
We have no direct reference to the tail, so we must walk all n nodes to find it. This is why some implementations also keep a tail pointer -- it makes tail insertion O(1).
9 / 20
Insert at Position -- O(n)
Insert a new node at index k by finding the node at index k-1 and rewiring pointers.
BEFORE: Insert 25 at index 2
head index 0 index 1 index 2
| | | |
v v v v
[10|*]---->[20|*]---->[30|*]---->[40|null]
Step 1: Traverse to node at index 1 (the node BEFORE insertion point)
prev = node at index 1 (value 20)
Step 2: Create new node and rewire
newNode = new Node(25);
newNode.next = prev.next; // new node points to node(30)
prev.next = newNode; // node(20) now points to new node
AFTER:
head
|
v
[10|*]---->[20|*]---->[25|*]---->[30|*]---->[40|null]
^
inserted here (index 2)
public void insertAt(int index, int data) {
if (index == 0) {
insertAtHead(data);
return;
}
Node newNode = new Node(data);
Node prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
newNode.next = prev.next; // link first!
prev.next = newNode; // then rewire
size++;
}
Warning: Order of Assignments
WRONG:prev.next = newNode; first -- this breaks the chain and you lose access to everything after prev.
RIGHT: Always set newNode.next FIRST to preserve the rest of the chain, THEN update prev.next.
10 / 20
Delete from Head -- O(1)
Remove the first node by advancing the head pointer.
BEFORE:
head
|
v
+------+---+ +------+---+ +------+------+
| 10 | *-+--->| 20 | *-+--->| 30 | null |
+------+---+ +------+---+ +------+------+
Step 1: Save reference to old head (to return its data)
Node removed = head;
Step 2: Advance head to next node
head = head.next;
AFTER:
head
|
v
[10|*]-/-> +------+---+ +------+------+
(orphan) | 20 | *-+--->| 30 | null |
+------+---+ +------+------+
The old node(10) is now unreachable.
In Java: garbage collector will reclaim it.
In C/C++: you MUST call free(removed) or delete removed.
public int deleteFromHead() {
if (head == null)
throw new NoSuchElementException();
int data = head.data;
head = head.next;
size--;
return data;
}
C / C++: You must manually free() / delete the removed node, or you get a memory leak.
11 / 20
Delete from Tail -- O(n)
We must find the second-to-last node so we can set its next to null.
BEFORE: Delete last node (30)
head
|
v
[10|*]--->[20|*]--->[30|null]
Step 1: Traverse to second-to-last node
current = head;
while (current.next.next != null)
current = current.next;
current stops at node(20) because current.next.next == null
Step 2: Remove the last node
current.next = null;
AFTER:
head
|
v
[10|*]--->[20|null] [30] (orphaned, garbage collected)
public int deleteFromTail() {
if (head == null)
throw new NoSuchElementException();
if (head.next == null) {
// Only one node
int data = head.data;
head = null;
size--;
return data;
}
Node current = head;
while (current.next.next != null) {
current = current.next;
}
int data = current.next.data;
current.next = null;
size--;
return data;
}
Why Do We Need the Previous Node?
In a singly linked list, there is no backwards pointer. We cannot go from the last node to the second-to-last. So we must traverse from the head and stop one node early. This is why deletion from the tail is O(n), not O(1).
A doubly linked list solves this by adding a prev pointer.
12 / 20
Delete by Value -- O(n)
Find the node containing a specific value and remove it from the list.
BEFORE: Delete node with value 20
head
|
v
[10|*]--->[20|*]--->[30|*]--->[40|null]
Step 1: Traverse with TWO pointers (prev and current)
prev = null, current = head
Iteration 1: current.data = 10, not 20. prev = current, current = current.next
Iteration 2: current.data = 20, FOUND!
prev -> [10] current -> [20]
Step 2: Bypass the target node
prev.next = current.next; // node(10) now points to node(30)
AFTER:
head
|
v
[10|*]--->[30|*]--->[40|null]
node(20) is orphaned and garbage collected.
public boolean deleteByValue(int target) {
if (head == null) return false;
if (head.data == target) {
head = head.next;
size--;
return true;
}
Node prev = head;
Node current = head.next;
while (current != null) {
if (current.data == target) {
prev.next = current.next;
size--;
return true;
}
prev = current;
current = current.next;
}
return false; // not found
}
The Two-Pointer Technique
We maintain prev and current because to remove a node, we need to update the previous node'snext pointer. We cannot go backwards in a singly linked list, so we track prev as we go.
13 / 20
Search / Contains -- O(n)
Linear search through the list to find a value.
Java Code
public boolean contains(int target) {
Node current = head;
while (current != null) {
if (current.data == target) {
return true; // found!
}
current = current.next;
}
return false; // not in list
}
public int indexOf(int target) {
Node current = head;
int index = 0;
while (current != null) {
if (current.data == target) {
return index;
}
current = current.next;
index++;
}
return -1; // not found
}
Unlike arrays where you can jump to arr[i] in O(1), a linked list requires you to start at the head and follow next pointers one by one. Searching is always O(n) in the worst case.
14 / 20
Get Size / Length
Two approaches: traverse and count, or maintain a size variable.
Approach 1: Count by Traversal -- O(n)
public int getSize() {
int count = 0;
Node current = head;
while (current != null) {
count++;
current = current.next;
}
return count;
}
public class SinglyLinkedList {
private Node head;
private int size; // track it!
public int getSize() {
return size; // O(1)!
}
public void insertAtHead(int data) {
// ... insertion logic ...
size++; // increment on insert
}
public int deleteFromHead() {
// ... deletion logic ...
size--; // decrement on delete
}
}
Best Practice
Always maintain a size field. Increment on every insert, decrement on every delete. This gives you O(1) size queries instead of O(n).
15 / 20
Sentinel / Dummy Head Node
A dummy node at the front simplifies code by eliminating special cases for an empty list or inserting/deleting at the head.
WITHOUT dummy (must check if head == null everywhere):
head = null (empty list -- special case!)
head -> [10|*] -> [20|null] (insert at head -- special case!)
WITH dummy node (head is NEVER null):
head
|
v
+-------+---+ +------+---+ +------+------+
| DUMMY | *-+--->| 10 | *-+--->| 20 | null | (actual data starts AFTER dummy)
+-------+---+ +------+---+ +------+------+
Empty list with dummy:
head
|
v
+-------+------+
| DUMMY | null | (still have a head node -- no special case!)
+-------+------+
// With dummy: insert at front
// No special case needed!
public void insertAtFront(int data) {
Node newNode = new Node(data);
newNode.next = head.next;
head.next = newNode;
size++;
}
// With dummy: delete by value
// No special case for head!
public boolean delete(int target) {
Node prev = head; // start at dummy
Node cur = head.next;
while (cur != null) {
if (cur.data == target) {
prev.next = cur.next;
size--;
return true;
}
prev = cur;
cur = cur.next;
}
return false;
}
Why Use a Sentinel?
The head pointer is never null -- no need to check if (head == null)
Inserting/deleting at the front follows the same logic as any other position
Code is simpler and less error-prone
The dummy node holds no real data -- it is just a structural placeholder
Trade-off: Uses one extra node of memory.
16 / 20
Common Pitfalls
These are the bugs that trip up nearly every student the first time they implement a linked list.
1. NullPointerException
Calling current.next when current is null. Always check current != null before accessing its fields.
2. Losing References
Overwriting head or a next pointer before saving the old value. The rest of the list becomes unreachable.
// BUG: Lost the rest of the list!
head = newNode; // old head is gone
newNode.next = head; // points to itself!
// FIX: Save before overwriting
newNode.next = head; // preserve chain
head = newNode; // now safe
3. Off-by-One Errors
When traversing to index k, you need to stop at index k-1 for insertion. Using < vs <= in loop conditions is a common source of bugs.
4. Forgetting to Update Size
If you maintain a size field, every insert must size++ and every delete must size--. One missed update corrupts all future size queries.
5. Not Handling Edge Cases
Empty list (head == null)
Single-element list
Deleting the head node
Inserting at index 0
17 / 20
Time Complexity Summary
Operation
Singly Linked List
Array (fixed)
ArrayList (dynamic)
Insert at head
O(1)
O(n)
O(n)
Insert at tail
O(n)*
O(1)†
O(1) amortized
Insert at index k
O(k)
O(n)
O(n)
Delete at head
O(1)
O(n)
O(n)
Delete at tail
O(n)
O(1)
O(1)
Delete by value
O(n)
O(n)
O(n)
Access by index
O(n)
O(1)
O(1)
Search
O(n)
O(n)
O(n)
Get size
O(1)‡
O(1)
O(1)
* O(1) if you maintain a tail pointer
† Only if space available
‡ If you maintain a size field
When to Choose a Linked List
Use a linked list when you need frequent insertions/deletions at the head, don't need random access by index, and the data size is unpredictable. Use an array when you need fast random access and the size is relatively stable.
18 / 20
Real-World Applications
Implementing Stacks & Queues
Stack (insert/delete at head):
push(3): head->[3]->[2]->[1]->null
pop(): head->[2]->[1]->null (returned 3)
Queue (insert at tail, delete at head):
enqueue(3): head->[1]->[2]->[3]->null
dequeue(): head->[2]->[3]->null (returned 1)
Music Playlist
[Song A] --> [Song B] --> [Song C] --> null
^
currently
playing
"Next track" = advance the pointer
"Add to queue" = insert at tail
"Remove track" = delete by value
Undo Functionality
Each action is a node:
head
|
v
[action3] --> [action2] --> [action1]
"Undo" = delete from head
"New action" = insert at head
STRUCTURE:
head -> [data|next] -> [data|next] -> null
NODE CLASS:
class Node {
int data;
Node next;
}
INSERT AT HEAD: O(1)
newNode.next = head;
head = newNode;
INSERT AT TAIL: O(n)
traverse to last node
last.next = newNode;
DELETE FROM HEAD: O(1)
head = head.next;
DELETE FROM TAIL: O(n)
traverse to second-to-last
secondToLast.next = null;
SEARCH: O(n)
traverse, compare each node
TRAVERSAL PATTERN:
Node cur = head;
while (cur != null) {
// process cur.data
cur = cur.next;
}
Golden Rules
Never lose the head -- always use a temporary pointer for traversal.
Link before you redirect -- set newNode.next before updating prev.next or head.
Check for null -- before accessing .data or .next, make sure the node is not null.
Handle edge cases -- empty list, single element, head deletion.
Maintain size -- increment and decrement on every insert and delete.
Train Cars Analogy Recap
Engine = head pointer (entry point)
Each car = a node (carries cargo/data)
Coupling = next pointer (connects to next car)
Last car = points to null (end of train)
Insert = couple a new car in
Delete = uncouple a car, rejoin the remaining cars
Coming Next
Doubly Linked Lists -- adding a prev pointer to enable backward traversal and O(1) tail operations.