Singly Linked Lists
A chain of nodes where each one holds data and a pointer to the next. How it's structured, how it differs from arrays, and when to use it.
What Is It?
A singly linked list is a chain of items where each item points to the next one.
Each item in the chain is called a node — a small container that holds two things:
- The actual value you want to store (a number, a word, anything)
- A pointer to the next node
A pointer is just the location of where something is stored in memory. Think of it like a sticky note that says "the next item is stored at location 42."
The very last node has no next node, so its pointer is set to null — meaning "nothing here, the chain ends."
The list itself only needs to remember one thing: where the first node is. We call this the head. If you lose the head, you lose the whole list.
The last node points to null, signaling the end of the list.
Each box above is a node. The left side holds the value, the right side holds the pointer to the next node. The last node points to null.
Analogy
Imagine a scavenger hunt. You start with a single clue (the head pointer). That clue leads you to a location (the first node), where you find a prize (the data) and another clue (the next pointer) telling you where to go next. Each location has a prize and a clue to the next spot. The last location has a prize but no further clue — that is your null terminator.
You cannot jump to location #5 directly. You must follow the chain from clue to clue, visiting each location in order. This is exactly how traversal works in a singly linked list.
How It Works
The Node Structure
Every linked list is built from nodes. Here is what a single node looks like:
structure Node:data // the value stored in this nodenext // pointer to the next Node (or null)
That is the entire building block. Everything else — inserting, deleting, searching — is just manipulating these nodes and their next pointers.
Creating a Linked List by Hand
Let us build the list 10 -> 20 -> 30 step by step:
// Step 1: Create three standalone nodesnodeA = new Node(10) // [10|null]nodeB = new Node(20) // [20|null]nodeC = new Node(30) // [30|null]// Step 2: Wire them togethernodeA.next = nodeB // [10|*]-->[20|null]nodeB.next = nodeC // [10|*]-->[20|*]-->[30|null]// Step 3: Set headhead = nodeA
After these steps, starting from head and following .next pointers gives us the full sequence: 10, 20, 30.
How It Compares to an Array
Array
- Memory layout — all together in one block
- Access by index — O(1) instant jump
- Insert at beginning — O(n) shift everything
- Insert at end — O(1) most of the time
- Size — fixed or must resize
Best for random access
Singly Linked List
- Memory layout — scattered anywhere
- Access by index — O(n) must walk from the start
- Insert at beginning — O(1) just update the head
- Insert at end — O(n) walk to the end first
- Size — grows and shrinks freely
Best for frequent insertions
An array lets you jump straight to element 5 by index. A linked list cannot do this — you have to start at the head and follow pointers one by one until you get there.
Traversal
To visit every node, you start at head and follow pointers until you hit null. Here is a concrete example with the list 10 -> 20 -> 30:
current = head // current points to [10]visit 10current = current.next // current now points to [20]visit 20current = current.next // current now points to [30]visit 30current = current.next // current is now null — stop!
In code, that loop looks like this:
function traverse(head):current = headwhile current is not null:process(current.data)current = current.next
This is O(n) — you visit each of the n nodes exactly once.
The Linked List Wrapper
In practice, you often wrap the head pointer in a structure:
structure LinkedList:head = nullsize = 0
This gives you a clean place to keep track of things. When the list is empty, head is null and size is 0.
Memory Layout Visualization
In an array, elements sit side by side:
Array: [10][20][30][40] <- all next to each other0 1 2 3 <- direct index access
In a linked list, nodes can be anywhere in memory:
Each node stores the location of the next node. The nodes can be anywhere in memory.
The next pointer stores the location of the next node. The nodes could be far apart — it does not matter as long as the pointers are correct.
Examples
Example 1: Counting Nodes
function countNodes(head):count = 0current = headwhile current is not null:count = count + 1current = current.nextreturn count
For the list 10 -> 20 -> 30, this returns 3.
Example 2: Finding a Value
function contains(head, target):current = headwhile current is not null:if current.data equals target:return truecurrent = current.nextreturn false
Example 3: Getting the Last Node
function getLast(head):if head is null:return nullcurrent = headwhile current.next is not null:current = current.nextreturn current
Notice the subtle difference: we check current.next (not current) so we stop AT the last node rather than going past it.
Common Mistakes
1. Losing the head pointer. If you reassign head during traversal, you lose access to the entire list. Always use a separate current variable for traversal.
// WRONG — destroys the list referencewhile head is not null:head = head.next // head is now null, list is lost!// CORRECTcurrent = headwhile current is not null:current = current.next // head is preserved
2. Not handling an empty list. If head is null, calling head.data or head.next will crash. Always check for null before accessing node properties.
3. Off-by-one in traversal. When you need the node BEFORE a certain position, stop one step earlier. When you need the last node, check current.next is not null instead of current is not null.
4. Forgetting to set next to null. When creating a new node, set next = null. Forgetting this can cause infinite loops or crashes.
Best Practices
Use a wrapper structure. Rather than passing a raw head pointer around, wrap it in a LinkedList structure that also tracks size. This prevents accidents and gives you a cleaner interface.
Always initialize next to null. When creating a new node, explicitly set next = null to ensure the list is always properly terminated.
Track the size. Maintaining a size counter means you never need to walk the whole list just to count how many items there are. Increment on insert, decrement on delete.
Draw it out. When debugging linked list code, draw the nodes and pointers on paper. Trace each pointer change step by step. Most linked list bugs come from pointer rewiring mistakes, and they are much easier to catch visually.
Runnable Code
Here is a complete JavaScript implementation you can run right here. It covers creating a linked list, appending, prepending, deleting, searching, and traversing.
class Node {constructor(data) {this.data = data;this.next = null;}}class LinkedList {constructor() {this.head = null;this.size = 0;}// Add to the endappend(data) {const node = new Node(data);if (!this.head) {this.head = node;} else {let current = this.head;while (current.next) current = current.next;current.next = node;}this.size++;}// Add to the beginningprepend(data) {const node = new Node(data);node.next = this.head;this.head = node;this.size++;}// Delete first occurrence of valuedelete(data) {if (!this.head) return;if (this.head.data === data) {this.head = this.head.next;this.size--;return;}let current = this.head;while (current.next && current.next.data !== data) {current = current.next;}if (current.next) {current.next = current.next.next;this.size--;}}// Search for a value, return index or -1search(data) {let current = this.head;let index = 0;while (current) {if (current.data === data) return index;current = current.next;index++;}return -1;}// Print the listtraverse() {let current = this.head;const values = [];while (current) {values.push(current.data);current = current.next;}console.log(values.join(' -> ') + ' -> null');}}// --- Demo ---const list = new LinkedList();list.append(10);list.append(20);list.append(30);console.log('After append 10, 20, 30:');list.traverse();list.prepend(5);console.log('After prepend 5:');list.traverse();list.delete(20);console.log('After delete 20:');list.traverse();console.log('Search for 30: found at index', list.search(30));console.log('Search for 99:', list.search(99) === -1 ? 'not found' : 'found');console.log('Size:', list.size);