Singly Linked Lists

CS205 Data Structures

+---------+ +---------+ +---------+ +---------+ | 10 | *--+--->| 20 | *--+--->| 30 | *--+--->| 40 |null| +---------+ +---------+ +---------+ +---------+ ^ | head

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.

2 / 20

Array vs. Linked List

Array: Contiguous Memory

Memory addresses (contiguous): 0x100 0x104 0x108 0x10C 0x110 +------+------+------+------+------+ | 10 | 20 | 30 | 40 | 50 | +------+------+------+------+------+ [0] [1] [2] [3] [4] Direct access: arr[3] = go to 0x10C

Linked List: Scattered Memory

Memory (scattered anywhere): 0x200 0x580 0x340 0x710 +------+--+ +------+--+ +------+--+ +------+----+ | 10 | *-->| 20 | *-->| 30 | *-->| 40 |null| +------+--+ +------+--+ +------+--+ +------+----+ Access [3]: must walk 0 -> 1 -> 2 -> 3
Operation Array Linked List Winner
Access by index O(1) O(n) Array
Insert at front O(n) O(1) Linked List
Insert at end O(1)* O(n) Array
Insert at middle O(n) O(n) Tie
Delete at front O(n) O(1) Linked List
Search O(n) O(n) Tie
Memory overhead Low Higher (pointers) Array

* 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; }

Memory Management

  • Java / Python: Garbage collector automatically frees unreachable nodes.
  • 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's next 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 }

Searching for value 30

head | v [10|*]--->[20|*]--->[30|*]--->[40|null] ^ cur: 10 == 30? No. [10|*]--->[20|*]--->[30|*]--->[40|null] ^ cur: 20 == 30? No. [10|*]--->[20|*]--->[30|*]--->[40|null] ^ cur: 30 == 30? YES! return true (index = 2)

No Random Access

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; }
[10|*]--->[20|*]--->[30|null] ^count=1 ^count=2 ^count=3 current=null -> return 3

Approach 2: Maintain Size Field -- O(1)

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

Polynomial Representation

3x^4 + 2x^2 + 5 [3,4] --> [2,2] --> [5,0] --> null coeff,exp coeff,exp coeff,exp Easy to add/remove terms!

Also Used In...

  • Operating system process scheduling
  • Hash table chaining (collision resolution)
  • Graph adjacency lists
  • Memory allocation (free lists)
19 / 20

Summary & Cheat Sheet

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.

20 / 20