Skip to content

Linked Lists

Understand pointer-based data structures — nodes, traversal, and why they matter for queues, LRU caches, and more.

Why Linked Lists?

Linked lists aren’t used as often as arrays in day-to-day code, but they’re the foundation for many critical data structures: queues, deques, LRU caches, adjacency lists for graphs, and more. Understanding how pointers connect nodes is essential for working with trees and graphs later.

Real-World Analogy

Like a scavenger hunt — each clue tells you where to find the next one, but you can’t jump to clue #5 directly. To insert a new clue in the middle, you just update two pointers — no need to renumber everything.

Singly Linked List

Each node holds a value and a pointer to the next node.

class ListNode<T> {
	value: T;
	next: ListNode<T> | null = null;

	constructor(value: T) {
		this.value = value;
	}
}

class LinkedList<T> {
	head: ListNode<T> | null = null;
	size = 0;

	// O(1) — prepend to front
	prepend(value: T): void {
		const node = new ListNode(value);
		node.next = this.head;
		this.head = node;
		this.size++;
	}

	// O(n) — append to end
	append(value: T): void {
		const node = new ListNode(value);
		if (!this.head) {
			this.head = node;
		} else {
			let current = this.head;
			while (current.next) current = current.next;
			current.next = node;
		}
		this.size++;
	}

	// O(n) — delete first occurrence
	delete(value: T): boolean {
		if (!this.head) return false;

		if (this.head.value === value) {
			this.head = this.head.next;
			this.size--;
			return true;
		}

		let current = this.head;
		while (current.next) {
			if (current.next.value === value) {
				current.next = current.next.next;
				this.size--;
				return true;
			}
			current = current.next;
		}
		return false;
	}
}

Fast & Slow Pointer Pattern

The most important linked list technique. Use two pointers moving at different speeds to detect cycles, find midpoints, and more.

// Detect cycle in a linked list
function hasCycle<T>(head: ListNode<T> | null): boolean {
	let slow = head;
	let fast = head;

	while (fast?.next) {
		slow = slow!.next;
		fast = fast.next.next;
		if (slow === fast) return true;
	}
	return false;
}

// Find the middle node
function findMiddle<T>(head: ListNode<T> | null): ListNode<T> | null {
	let slow = head;
	let fast = head;

	while (fast?.next) {
		slow = slow!.next;
		fast = fast.next.next;
	}
	return slow;
}

// Reverse a linked list (iterative)
function reverse<T>(head: ListNode<T> | null): ListNode<T> | null {
	let prev: ListNode<T> | null = null;
	let current = head;

	while (current) {
		const next = current.next;
		current.next = prev;
		prev = current;
		current = next;
	}
	return prev;
}

Reversing a linked list comes up constantly — in interviews and in real code (e.g., reversing a segment of an undo history). Practice until you can write it without thinking.

Arrays vs. Linked Lists

OperationArrayLinked List
Access by indexO(1)O(n)
Insert at frontO(n)O(1)
Insert at endO(1)*O(n) or O(1)**
Delete from frontO(n)O(1)
SearchO(n)O(n)
MemoryContiguousScattered

* Amortized for dynamic arrays. ** O(1) if you maintain a tail pointer.

Key Takeaways

  1. Linked lists excel at front insertions/deletions — O(1) vs arrays’ O(n)
  2. Fast & slow pointers solve cycle detection and midpoint finding elegantly
  3. Reversing a linked list is the most common linked list operation — know it cold
  4. Use arrays by default — linked lists are building blocks for more complex structures, rarely used alone