Linked Lists
A linked list is a linear data structure where elements are stored in nodes, and each node points to the next node via a reference (pointer). Unlike arrays, linked list nodes are not stored in contiguous memory — they can live anywhere in the heap, connected only by these pointers.
Arrays vs Linked Lists — The Core Difference
To understand why linked lists exist, you first need to see what makes arrays limiting in certain situations.
Arrays allocate a fixed block of contiguous memory. The runtime computes any element's address as base + index * elementSize, which is why random access is O(1). The downside: inserting or deleting in the middle requires shifting every element after the insertion point — an O(n) operation just to maintain contiguity.
Linked lists trade that random-access speed for dynamic, O(1) insert-and-delete at the head. Each node allocates independently, so there is no shifting. The cost is that you must traverse from the head to reach any node — O(n) access instead of O(1).
Property | Array | Linked List |
|---|---|---|
Memory layout | Contiguous block | Scattered nodes on the heap |
Size | Fixed (or amortized dynamic) | Grows and shrinks freely |
Random access | O(1) — index arithmetic | O(n) — must traverse |
Insert at head | O(n) — shift all elements | O(1) — update pointer |
Insert at tail | O(1) amortized (dynamic array) | O(n) without tail pointer |
Insert at middle | O(n) — shift elements | O(n) traversal + O(1) pointer swap |
Delete at head | O(n) — shift all elements | O(1) — update head pointer |
Cache performance | Excellent (spatial locality) | Poor (pointer chasing) |
Anatomy of a Node
Every linked list is built from nodes. A node holds two things:
val (or
data) — the actual value stored in this nodenext — a reference to the next node, or
nullif this is the last node
class Node {
constructor(val) {
this.val = val // the data this node carries
this.next = null // pointer to the next node (null = end of list)
}
}Visually, a three-node list holding the values 10 → 20 → 30 looks like this:
head | v +------+------+ +------+------+ +------+------+ | 10 | *---+--->| 20 | *---+--->| 30 | null | +------+------+ +------+------+ +------+------+ Node 1 Node 2 Node 3 (tail)
Each box represents one Node object. The left cell is val; the right cell is next — shown as *--- (a pointer arrow) when it points to another node, or null when there is no next node.
Head and Tail Pointers
A LinkedList object typically maintains two special references:
head — points to the first node. Without
head, you cannot reach any node in the list.tail — points to the last node. Without
tail, appending to the end requires traversing the entire list every time.
LinkedList
+----------+ +------+------+ +------+------+ +------+------+
| head *---+----->| 10 | *---+--->| 20 | *---+--->| 30 | null |
| tail *---+------+------+------+ +------+------+ +------+------+
| size = 3 | ^
+----------+ |
tail points here
When the list is empty, both head and tail are null and size is 0.
Traversal
Because there is no index arithmetic, the only way to visit every node is to start at head and follow next pointers until you reach null.
function traverse(head) {
let current = head // start at the first node
while (current !== null) {
console.log(current.val) // process the current node
current = current.next // move to the next node
}
}
// Time: O(n) — visits every node once
// Space: O(1) — only one pointer variableTypes of Linked Lists
There are three main variants, each adding a structural capability on top of the last:
1. Singly Linked List
Each node has one pointer: next. Traversal is one-directional — forward only. This is the simplest and most memory-efficient variant.
[10] --> [20] --> [30] --> null
O(1) insert at head
O(n) insert at tail (without tail pointer)
Cannot traverse backwards
Memory per node: val + 1 pointer
2. Doubly Linked List
Each node has two pointers: next (forward) and prev (backward). You can traverse in both directions, and deletion of a known node becomes O(1) instead of O(n) because you no longer need to find the previous node.
null <-- [10] <--> [20] <--> [30] --> null
O(1) insert and delete at both head and tail
Bidirectional traversal
Used to implement LRU caches, browser history, undo/redo stacks
Memory per node: val + 2 pointers
3. Circular Linked List
The tail node's next points back to the head instead of null. There is no natural end — traversal must check for the head pointer to avoid an infinite loop. Circular doubly linked lists (where tail points to head AND head points to tail) power many OS schedulers and round-robin algorithms.
+------------------------------------+ | | v | [10] --> [20] --> [30] --> [40] ------+
No null terminator — every node has a valid
nextUseful for rotating structures: music playlists, CPU scheduling
Must track a "start" reference to know when a full cycle is complete
Complexity Summary
This table compares a singly linked list with a tail pointer against a dynamic array (like JavaScript arrays or Java ArrayList):
Operation | Array | Singly Linked List | Notes |
|---|---|---|---|
Access by index | O(1) | O(n) | Arrays win — index arithmetic is instant |
Search by value | O(n) | O(n) | Both must scan linearly |
Insert at head | O(n) | O(1) | Linked list wins — update head pointer only |
Insert at tail | O(1) amortized | O(1) with tail ptr / O(n) without | Dynamic array amortized append vs tail pointer |
Insert at index i | O(n) | O(n) | Both: traverse to position first |
Delete at head | O(n) | O(1) | Linked list wins — advance head pointer |
Delete at tail | O(1) amortized | O(n) singly / O(1) doubly | Singly linked list must find second-to-last node |
Delete by value | O(n) | O(n) | Both: scan then adjust |
Space overhead | Low | Higher (pointer per node) | Each node allocates a pointer-sized reference |
When to Use a Linked List
Linked lists are the right tool in specific situations. They are not a general-purpose replacement for arrays.
Reach for a linked list when:
You need constant-time insertions or deletions at the head (or head and tail in a doubly linked list)
The number of elements is highly unpredictable and you want to avoid array reallocation
You are implementing a stack or queue and raw performance matters more than index-based access
You are building LRU cache eviction, an undo stack, or a playlist data structure
Stick with an array when:
You need frequent random access (
arr[i]) — arrays are O(1), linked lists are O(n)Cache performance is critical — arrays have excellent spatial locality
You need to sort the data — sorting algorithms assume random access
The size is known up front and small — array overhead is lower
A Minimal Singly Linked List
Here is the skeleton you will expand in the next page:
class Node {
constructor(val) {
this.val = val
this.next = null
}
}
class LinkedList {
constructor() {
this.head = null // first node
this.tail = null // last node (saves O(n) appends)
this.size = 0 // number of nodes
}
}
// Create an empty list
const list = new LinkedList()
// list.head === null, list.tail === null, list.size === 0Key Takeaways
A linked list is a chain of nodes connected by pointers — no contiguous memory required.
headis the entry point;tailenables O(1) appends;sizeavoids O(n) length calculations.Singly linked lists are one-directional; doubly linked lists add a
prevpointer for O(1) tail deletion.Circular linked lists have no
nullterminator — perfect for round-robin and scheduling.Prefer linked lists for O(1) head operations; prefer arrays for O(1) random access.
Every linked list algorithm uses the same traversal pattern: follow
nextuntilnull.