Nodes connected by pointers -- the fundamental dynamic data structure for efficient insertions and deletions.
A linked list is a linear data structure where each element (called a node) contains two things: the data itself and a pointer (reference) to the next node in the sequence. Unlike arrays, linked list nodes are not stored contiguously in memory -- they can be scattered anywhere in the heap.
Each node holds:
null if it is the last node)The head pointer always references the first node. The tail is the last node whose next pointer is null. Some implementations also maintain an explicit tail pointer for O(1) append operations.
Arrays give you O(1) random access because elements sit next to each other in memory. Linked lists sacrifice random access (O(n) to reach index i) but gain O(1) insertions/deletions at the head -- no shifting elements required.
Each node stores data and a single pointer to the next node. Traversal is one-directional -- you can only go forward.
Each node stores data, a pointer to the next node, and a pointer to the previous node. Traversal goes both directions.
| Aspect | Singly Linked List | Doubly Linked List |
|---|---|---|
| Memory per node | Less (1 pointer) | More (2 pointers) |
| Forward traversal | Yes | Yes |
| Backward traversal | No | Yes |
| Delete given node | O(n) -- need predecessor | O(1) -- has prev pointer |
| Insert before node | O(n) -- need predecessor | O(1) -- has prev pointer |
| Implementation | Simpler | More complex |
| Use case | Stacks, simple lists | LRU cache, browser history |
| Operation | Singly LL | Doubly LL | Array |
|---|---|---|---|
| Access by index | O(n) | O(n) | O(1) |
| Insert at head | O(1) | O(1) | O(n) |
| Insert at tail | O(n) / O(1)* | O(1) | O(1) amortized |
| Insert at middle | O(n) | O(n) | O(n) |
| Delete head | O(1) | O(1) | O(n) |
| Delete tail | O(n) | O(1) | O(1) |
| Search | O(n) | O(n) | O(n) |
* Singly linked list insert at tail is O(1) only if you maintain a tail pointer. Without one, you must traverse the entire list to find the end, making it O(n).
Even though linked lists have great theoretical complexity for insertions/deletions, arrays often win in practice because of CPU cache locality. Array elements sit next to each other in memory, so the CPU can prefetch them efficiently. Linked list nodes are scattered in memory, causing frequent cache misses.
All linked list "O(1) insertion/deletion" claims assume you already hold a pointer to the node. Finding the node first is O(n). Without a pre-held pointer, insertion at position i is O(i), not O(1).
Rule: If you need O(1) append, you MUST maintain a tail pointer. Without it, append is O(n).
We build a Node class (data + next pointer) and a LinkedList class with core operations: append, prepend, insert_at, delete, search, display, and length.
Python
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
self._length = 0
def append(self, data):
"""Add node to the end of the list."""
new_node = Node(data)
self._length += 1
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def prepend(self, data):
"""Add node to the beginning of the list. O(1)."""
new_node = Node(data)
new_node.next = self.head
self.head = new_node
self._length += 1
def insert_at(self, index, data):
"""Insert node at a specific index."""
if index < 0 or index > self._length:
raise IndexError("Index out of bounds")
if index == 0:
self.prepend(data)
return
new_node = Node(data)
current = self.head
for _ in range(index - 1):
current = current.next
new_node.next = current.next
current.next = new_node
self._length += 1
def delete(self, data):
"""Delete the first node with the given value."""
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
self._length -= 1
return
current = self.head
while current.next:
if current.next.data == data:
current.next = current.next.next
self._length -= 1
return
current = current.next
def search(self, data):
"""Return True if value exists in the list."""
current = self.head
while current:
if current.data == data:
return True
current = current.next
return False
def display(self):
"""Print the list as: 1 -> 2 -> 3 -> null"""
parts = []
current = self.head
while current:
parts.append(str(current.data))
current = current.next
print(" -> ".join(parts) + " -> null")
def length(self):
"""Return the number of nodes."""
return self._length
# Usage
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.prepend(0)
ll.insert_at(2, 99)
ll.display() # 0 -> 1 -> 99 -> 2 -> 3 -> null
ll.delete(99)
ll.display() # 0 -> 1 -> 2 -> 3 -> null
print(ll.search(2)) # True
print(ll.length()) # 4
1. Create new node with data=3, next=null.
2. If head is null, set head = new node. Done.
3. Otherwise, start at head. Walk forward until you find a node whose next is null (the tail).
4. Set that tail node's next to the new node. The new node becomes the new tail.
JavaScript
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this._length = 0;
}
append(data) {
const newNode = new Node(data);
this._length++;
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
prepend(data) {
const newNode = new Node(data);
newNode.next = this.head;
this.head = newNode;
this._length++;
}
insertAt(index, data) {
if (index < 0 || index > this._length) {
throw new Error("Index out of bounds");
}
if (index === 0) {
this.prepend(data);
return;
}
const newNode = new Node(data);
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current.next;
}
newNode.next = current.next;
current.next = newNode;
this._length++;
}
delete(data) {
if (!this.head) return;
if (this.head.data === data) {
this.head = this.head.next;
this._length--;
return;
}
let current = this.head;
while (current.next) {
if (current.next.data === data) {
current.next = current.next.next;
this._length--;
return;
}
current = current.next;
}
}
search(data) {
let current = this.head;
while (current) {
if (current.data === data) return true;
current = current.next;
}
return false;
}
display() {
const parts = [];
let current = this.head;
while (current) {
parts.push(current.data);
current = current.next;
}
console.log(parts.join(" -> ") + " -> null");
}
length() {
return this._length;
}
}
// Usage
const ll = new LinkedList();
ll.append(1);
ll.append(2);
ll.append(3);
ll.prepend(0);
ll.insertAt(2, 99);
ll.display(); // 0 -> 1 -> 99 -> 2 -> 3 -> null
ll.delete(99);
ll.display(); // 0 -> 1 -> 2 -> 3 -> null
console.log(ll.search(2)); // true
console.log(ll.length()); // 4
The doubly linked list adds a prev pointer to each node. This enables O(1) deletion of a given node and backward traversal, at the cost of extra memory per node.
Python
class DNode:
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
self._length = 0
def append(self, data):
"""Add node to end. O(1) with tail pointer."""
new_node = DNode(data)
self._length += 1
if not self.head:
self.head = new_node
self.tail = new_node
return
new_node.prev = self.tail
self.tail.next = new_node
self.tail = new_node
def prepend(self, data):
"""Add node to beginning. O(1)."""
new_node = DNode(data)
self._length += 1
if not self.head:
self.head = new_node
self.tail = new_node
return
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
def delete_node(self, node):
"""Delete a specific node reference. O(1)."""
if node.prev:
node.prev.next = node.next
else:
self.head = node.next
if node.next:
node.next.prev = node.prev
else:
self.tail = node.prev
self._length -= 1
def delete_value(self, data):
"""Delete first node with given value."""
current = self.head
while current:
if current.data == data:
self.delete_node(current)
return
current = current.next
def search(self, data):
"""Return True if value exists."""
current = self.head
while current:
if current.data == data:
return True
current = current.next
return False
def display_forward(self):
parts = []
current = self.head
while current:
parts.append(str(current.data))
current = current.next
print("null <-> " + " <-> ".join(parts) + " <-> null")
def display_backward(self):
parts = []
current = self.tail
while current:
parts.append(str(current.data))
current = current.prev
print("null <-> " + " <-> ".join(parts) + " <-> null")
def length(self):
return self._length
JavaScript
class DNode {
constructor(data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this._length = 0;
}
append(data) {
const newNode = new DNode(data);
this._length++;
if (!this.head) {
this.head = newNode;
this.tail = newNode;
return;
}
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
prepend(data) {
const newNode = new DNode(data);
this._length++;
if (!this.head) {
this.head = newNode;
this.tail = newNode;
return;
}
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
deleteNode(node) {
if (node.prev) {
node.prev.next = node.next;
} else {
this.head = node.next;
}
if (node.next) {
node.next.prev = node.prev;
} else {
this.tail = node.prev;
}
this._length--;
}
deleteValue(data) {
let current = this.head;
while (current) {
if (current.data === data) {
this.deleteNode(current);
return;
}
current = current.next;
}
}
search(data) {
let current = this.head;
while (current) {
if (current.data === data) return true;
current = current.next;
}
return false;
}
displayForward() {
const parts = [];
let current = this.head;
while (current) {
parts.push(current.data);
current = current.next;
}
console.log("null <-> " + parts.join(" <-> ") + " <-> null");
}
displayBackward() {
const parts = [];
let current = this.tail;
while (current) {
parts.push(current.data);
current = current.prev;
}
console.log("null <-> " + parts.join(" <-> ") + " <-> null");
}
length() {
return this._length;
}
}
This technique uses two pointers that move at different speeds through the list. The slow pointer moves one step at a time, while the fast pointer moves two steps. This pattern is used to detect cycles and find the middle of a linked list.
If a cycle exists, the fast pointer will eventually "lap" the slow pointer and they will meet. If no cycle exists, the fast pointer reaches null.
Python
def has_cycle(head):
"""Floyd's cycle detection. O(n) time, O(1) space."""
slow = head
fast = head
while fast and fast.next:
slow = slow.next # Move 1 step
fast = fast.next.next # Move 2 steps
if slow == fast:
return True # Cycle detected
return False # No cycle
JavaScript
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next; // Move 1 step
fast = fast.next.next; // Move 2 steps
if (slow === fast) {
return true; // Cycle detected
}
}
return false; // No cycle
}
When fast reaches the end, slow is at the middle. If the list has even length, slow will be at the second of the two middle nodes.
Python
def find_middle(head):
"""Return middle node. O(n) time, O(1) space."""
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is now at the middle
JavaScript
function findMiddle(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
Fast moves 2x the speed of slow. When fast has traversed the entire list (n steps), slow has traversed n/2 steps -- exactly the middle.
This is one of the most important linked list operations. We use three pointers: prev, curr, and next_node. At each step, we reverse one link.
Python
def reverse_list(head):
"""Reverse a singly linked list. O(n) time, O(1) space."""
prev = None
curr = head
while curr:
next_node = curr.next # Save next
curr.next = prev # Reverse the link
prev = curr # Move prev forward
curr = next_node # Move curr forward
return prev # prev is the new head
JavaScript
function reverseList(head) {
let prev = null;
let curr = head;
while (curr !== null) {
const nextNode = curr.next; // Save next
curr.next = prev; // Reverse the link
prev = curr; // Move prev forward
curr = nextNode; // Move curr forward
}
return prev; // prev is the new head
}
Forgetting to save curr.next before overwriting it. If you set curr.next = prev first, you lose the reference to the rest of the list. Always save the next pointer first.
Use a two-pointer approach with a dummy head node. Compare current nodes from both lists, attach the smaller one to the merged list, and advance that pointer.
Python
def merge_two_lists(l1, l2):
"""Merge two sorted lists into one sorted list."""
dummy = Node(0) # Dummy head simplifies edge cases
current = dummy
while l1 and l2:
if l1.data <= l2.data:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# Attach the remaining nodes
current.next = l1 if l1 else l2
return dummy.next # Skip the dummy node
JavaScript
function mergeTwoLists(l1, l2) {
const dummy = new Node(0);
let current = dummy;
while (l1 !== null && l2 !== null) {
if (l1.data <= l2.data) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
// Attach remaining nodes
current.next = l1 !== null ? l1 : l2;
return dummy.next;
}
The dummy node eliminates special-case handling for the first node. Instead of checking "is the merged list empty?", we always have a node to attach to. At the end, we return dummy.next to skip it.
These are the essential linked list problems. Master these and you will handle most linked list interview questions.
| Problem | Difficulty | Key Technique |
|---|---|---|
| 206. Reverse Linked List | Easy | Three pointers (prev, curr, next) |
| 21. Merge Two Sorted Lists | Easy | Two pointers + dummy node |
| 141. Linked List Cycle | Easy | Fast/slow pointers |
| 19. Remove Nth Node From End | Medium | Two pointers with n-gap |
| 143. Reorder List | Medium | Find middle + reverse second half + merge |
| 2. Add Two Numbers | Medium | Traverse both lists, carry digit |
| 146. LRU Cache | Medium | Doubly linked list + hash map |
Problem: Given the head of a singly linked list, reverse the list and return the reversed list.
Approach: Use three pointers -- prev, curr, and next_node. Walk through the list, reversing each link as you go. When curr becomes null, prev points to the new head.
Input: 1 -> 2 -> 3 -> 4 -> 5 -> null
Output: 5 -> 4 -> 3 -> 2 -> 1 -> null
Python
class Solution:
def reverseList(self, head):
# Initialize prev to None -- this becomes the new tail
prev = None
curr = head
while curr:
# 1) Save the next node before we break the link
next_node = curr.next
# 2) Reverse the link: point curr backwards
curr.next = prev
# 3) Advance prev and curr one step forward
prev = curr
curr = next_node
# When loop ends, curr is None and prev is the new head
return prev
# Time: O(n) -- visit each node once
# Space: O(1) -- only use pointer variables
JavaScript
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr !== null) {
// 1) Save next before breaking the link
const nextNode = curr.next;
// 2) Reverse the link
curr.next = prev;
// 3) Move pointers forward
prev = curr;
curr = nextNode;
}
// prev is the new head
return prev;
};
// Time: O(n) | Space: O(1)
At each step, we are "peeling off" the current node from the front of the remaining list and attaching it to the front of the reversed list (pointed to by prev). After processing all nodes, prev is the head of the fully reversed list.
Problem: Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.
Approach: Create a dummy node as the anchor. Compare the front of both lists, attach the smaller node, advance that pointer. When one list is exhausted, attach the rest of the other.
Input: list1 = 1 -> 2 -> 4, list2 = 1 -> 3 -> 4
Output: 1 -> 1 -> 2 -> 3 -> 4 -> 4
Python
class Solution:
def mergeTwoLists(self, list1, list2):
# Dummy node eliminates edge cases for the first node
dummy = ListNode(0)
tail = dummy
while list1 and list2:
if list1.val <= list2.val:
# Attach list1's current node
tail.next = list1
list1 = list1.next
else:
# Attach list2's current node
tail.next = list2
list2 = list2.next
tail = tail.next
# One list might still have nodes remaining
tail.next = list1 if list1 else list2
return dummy.next # Skip the dummy
# Time: O(n + m) where n, m are list lengths
# Space: O(1) -- we reuse existing nodes
JavaScript
var mergeTwoLists = function(list1, list2) {
const dummy = new ListNode(0);
let tail = dummy;
while (list1 !== null && list2 !== null) {
if (list1.val <= list2.val) {
tail.next = list1;
list1 = list1.next;
} else {
tail.next = list2;
list2 = list2.next;
}
tail = tail.next;
}
// Attach remaining nodes
tail.next = list1 !== null ? list1 : list2;
return dummy.next;
};
// Time: O(n + m) | Space: O(1)
list1: 1 -> 2 -> 4 list2: 1 -> 3 -> 4
Step 1: Compare 1 vs 1. Pick list1 (<=). Result: 1
Step 2: Compare 2 vs 1. Pick list2. Result: 1 -> 1
Step 3: Compare 2 vs 3. Pick list1. Result: 1 -> 1 -> 2
Step 4: Compare 4 vs 3. Pick list2. Result: 1 -> 1 -> 2 -> 3
Step 5: Compare 4 vs 4. Pick list1 (<=). Result: 1 -> 1 -> 2 -> 3 -> 4
Step 6: list1 exhausted. Attach list2 remainder: 1 -> 1 -> 2 -> 3 -> 4 -> 4
Problem: Given the head of a linked list, determine if the linked list has a cycle in it. A cycle exists if some node can be reached again by continuously following the next pointer.
Approach: Use Floyd's Tortoise and Hare algorithm. The slow pointer moves one step, the fast pointer moves two steps. If they ever meet, there is a cycle. If fast reaches null, there is no cycle.
Input: 3 -> 2 -> 0 -> -4 -> (back to node 2)
Output: true
Input: 1 -> 2 -> null
Output: false
Python
class Solution:
def hasCycle(self, head):
# Two pointers starting at head
slow = head
fast = head
while fast and fast.next:
slow = slow.next # Tortoise: 1 step
fast = fast.next.next # Hare: 2 steps
if slow == fast:
return True # They met -- cycle exists!
# Fast reached the end -- no cycle
return False
# Time: O(n) -- at most 2 passes through the list
# Space: O(1) -- only two pointers
JavaScript
var hasCycle = function(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next; // Tortoise: 1 step
fast = fast.next.next; // Hare: 2 steps
if (slow === fast) {
return true; // Cycle detected
}
}
return false; // No cycle
};
// Time: O(n) | Space: O(1)
Once both pointers are inside the cycle, the distance between them decreases by 1 each iteration (fast gains 1 step on slow). So they are guaranteed to meet within one full loop of the cycle. This is why it is O(n) and not infinite.
Technique: Use two pointers separated by a gap of n. Advance the first pointer n steps ahead. Then move both pointers together until the first reaches the end. The second pointer is now at the node just before the one to remove.
Technique: Three-step process:
Input: 1 -> 2 -> 3 -> 4 -> 5 becomes 1 -> 5 -> 2 -> 4 -> 3
Technique: Traverse both lists simultaneously, adding corresponding digits plus any carry. Create new nodes for each digit of the result. Remember to handle the final carry (e.g., 99 + 1 = 100 creates an extra node).
Technique: Combine a doubly linked list (for O(1) removal and reinsertion to track recency) with a hash map (for O(1) key lookup). The most recently used item goes to the front; when capacity is exceeded, remove from the back (least recently used).
LRU Cache is one of the most commonly asked medium-difficulty questions. It tests your ability to combine two data structures. Practice implementing it from scratch -- interviewers expect you to write the doubly linked list operations, not use a built-in OrderedDict.
In practice, dynamic arrays (Python lists, JavaScript arrays, Java ArrayList) are preferred for most use cases because of CPU cache benefits. Linked lists shine in specialized scenarios: undo/redo systems, memory allocators, hash table chaining, and implementing other data structures like stacks, queues, and LRU caches.