
A linked list is a data structure consisting of nodes, where each node contains a value and a reference (pointer) to the next node. Unlike arrays, linked lists do not store elements contiguously in memory. This makes insertion and deletion operations more flexible but requires careful pointer management.
Key points to keep in mind:
next pointers until null is reachedUnderstanding these fundamentals is crucial before diving into more advanced linked list patterns such as two pointers, reversals, and cycle detection.
Before tackling advanced patterns, it’s important to master the basic operations on a linked list.
All the snippets below are based on a ListNode data structure with the following definition.
class ListNode {
val: number;
next: ListNode | null;
constructor(val: number) {
this.val = val;
this.next = null;
}
}
When working with linked lists, edge cases often arise when inserting or deleting nodes at the head of the list. To handle these cases cleanly, we introduce a dummy node: a temporary node placed before the head. This allows all operations to be implemented uniformly, without special checks for the head.
For traversing a linked list, you typically start from the head and follow the next pointers until you reach the end (null).
function traverse(head: ListNode | null): void {
const dummy = new ListNode(0);
dummy.next = head;
let curr: ListNode | null = dummy.next;
while (curr) {
console.log(curr.val);
curr = curr.next;
}
};
The same technique can be applied to other operations like insertion and deletion, making the code cleaner and easier to manage. For example to insert a new node at a given position:
function insertAtPosition(head: ListNode | null, val: number, pos: number): ListNode | null {
const dummy = new ListNode(0);
dummy.next = head;
let prev: ListNode | null = dummy;
let index = 0;
while (prev && index < pos) {
prev = prev.next;
index++;
}
if (prev) {
const newNode = new ListNode(val);
newNode.next = prev.next;
prev.next = newNode;
}
return dummy.next;
}
The same dummy node approach can be used for deletion by adjusting the next pointers to skip over the target node:
function deleteByValue(head: ListNode | null, target: number): ListNode | null {
const dummy = new ListNode(0);
dummy.next = head;
let prev: ListNode | null = dummy;
let curr: ListNode | null = dummy.next;
while (curr) {
if (curr.val === target) {
prev!.next = curr.next;
break;
}
prev = curr;
curr = curr.next;
}
return dummy.next;
}
Reversing a linked list (fully or partially) is one of the most important skills when working with linked lists. A lot of problems require reversing the entire list or a portion of it.
The algorithm to reverse a linked list involves three main steps:
function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null;
let curr: ListNode | null = head;
while (curr !== null) {
const nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
For partial reversal, such as reversing a sublist from position m to n, we can use a similar approach but with additional pointers to manage the boundaries of the sublist.
function reverseBetween(
head: ListNode | null,
m: number,
n: number
): ListNode | null {
const dummy = new ListNode(0);
dummy.next = head;
let prev: ListNode | null = dummy;
for (let i = 1; i < m; i++) {
prev = prev!.next;
}
let start = prev!.next; // first node of the sublist
let then = start!.next; // node to be repositioned
for (let i = 0; i < n - m; i++) {
start!.next = then!.next;
then!.next = prev!.next;
prev!.next = then;
then = start!.next;
}
return dummy.next;
}
Cycle detection is a classic linked list problem: given a list, determine whether a node eventually points back to a previous one. The most common and efficient solution is Floyd’s Cycle Finding Algorithm, also known as the Tortoise and Hare approach.
Use two pointers:
Once a cycle is detected, it is possible to find the starting node of the cycle by:
function hasCycle(head: ListNode | null): boolean {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow!.next;
fast = fast.next.next;
if (slow === fast) {
return true;
}
}
return false;
}
Linked lists trade constant-time pointer manipulation for linear-time traversal. Understanding where each cost comes from is essential to choosing the right data structure and technique.
| Operation / Pattern | Time Complexity | Space Complexity |
|---|---|---|
| Traverse list | O(n) | O(1) |
| Insert at head | O(1) | O(1) |
| Insert at tail | O(n) | O(1) |
| Insert at position (with dummy node) | O(n) | O(1) |
| Delete by value (with dummy node) | O(n) | O(1) |
| Reverse linked list | O(n) | O(1) |
| Reverse sublist (m → n) | O(n) | O(1) |
| Cycle detection (Floyd’s algorithm) | O(n) | O(1) |
| Exercise | Difficulty | Description |
|---|---|---|
| Add Two Numbers | Medium | You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers... |
| Copy List With Random Pointer | Medium | A linked list of length |
| Design Linked List | Medium | Design your implementation of the linked list. You can choose to use a singly or doubly linked list. |
| Flatten a Multilevel Doubly Linked List | Medium | You are given a doubly linked list, which contains nodes that have a next pointer, a previous pointer, and an additional child pointer. This child pointer may or may not point to a separate dou... |
| Intersection of Two Linked Lists | Easy | Given the heads of two singly linked-lists |
| Partition List | Medium | Given the |
| Remove Duplicates from Sorted List II | Medium | Given the |
| Remove Nth Node From End of List | Medium | Given the |
| Rotate List | Medium | Given the |
| Swap Nodes in Pairs | Medium | Given a linked list, swap every two adjacent nodes and return its head. You must solve the problem without modifying the values in the list's nodes (i.e., only nodes themselves may be changed.) |