Linked Lists, Stacks, and Queues¶
Linked lists, stacks, and queues are the building blocks of more complex data structures. This file covers their mechanics, then builds up the key patterns; fast/slow pointers, monotonic stacks, and heap-based priority queues, through progressively harder problems, with common pitfalls at each step.
- Arrays give you fast random access but expensive insertions. Linked lists give you fast insertions but no random access. Stacks and queues restrict access to one or two ends, and this restriction is what makes them powerful: by limiting what you can do, they simplify what you need to think about.
Linked Lists¶
- A singly linked list is a chain of nodes. Each node stores a value and a pointer to the next node. The last node points to
null.
-
Advantages over arrays: inserting or deleting at a known position is \(O(1)\) (just repoint the pointers). No need to shift elements.
-
Disadvantages: accessing element \(i\) requires \(O(i)\) traversal (no random access). Poor cache locality (nodes are scattered in memory).
-
Doubly linked lists add a
prevpointer, enabling backward traversal. Used in LRU caches (constant-time removal of any node) and browser history (back/forward).
| Operation | Singly | Doubly |
|---|---|---|
| Access by index | \(O(n)\) | \(O(n)\) |
| Insert at head | \(O(1)\) | \(O(1)\) |
| Insert at tail | \(O(n)\) or \(O(1)\)* | \(O(1)\) |
| Delete given node | \(O(n)\)** | \(O(1)\) |
| Search | \(O(n)\) | \(O(n)\) |
with tail pointer. *need predecessor, which requires traversal.
- Sentinel nodes (dummy head/tail) simplify edge cases. Without a dummy head, inserting at the head or deleting the head requires special-case code. With a dummy, every real node has a predecessor.
# Without dummy: special case for head deletion
def delete_head(head):
if not head:
return None
return head.next
# With dummy: uniform logic
dummy = ListNode(0)
dummy.next = head
# now every deletion is: prev.next = prev.next.next
- Pitfall: forgetting to handle the empty list (
head is None) or the single-element list. Always test these edge cases.
Pattern: Fast and Slow Pointers (Floyd's)¶
- Use two pointers moving at different speeds to detect properties of linked lists. The slow pointer moves one step at a time; the fast pointer moves two steps.
Easy: Linked List Cycle¶
-
Problem: determine if a linked list has a cycle.
-
Pattern: if there is a cycle, the fast pointer will eventually lap the slow pointer (they will meet). If there is no cycle, the fast pointer reaches
null.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
-
Why it works: if the cycle has length \(c\), the fast pointer closes the gap by 1 node per step. They must meet within \(c\) steps of the slow pointer entering the cycle.
-
Pitfall: checking
fast and fast.next(not justfast.next). IffastisNone, callingfast.nextcrashes.
Medium: Find the Middle of a Linked List¶
-
Problem: return the middle node.
-
Pattern: when the fast pointer reaches the end, the slow pointer is at the middle.
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is at the middle (or second middle if even length)
Medium: Linked List Cycle II (Find Cycle Start)¶
-
Problem: return the node where the cycle begins.
-
Pattern: after fast and slow meet, reset one pointer to the head. Move both at speed 1. They meet at the cycle start.
def detect_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# reset one pointer to head
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
- Why this works: let the distance from head to cycle start be \(a\), and the distance from cycle start to meeting point be \(b\). The slow pointer travelled \(a + b\). The fast pointer travelled \(2(a + b)\). The difference is one full cycle: \(a + b = c\) (cycle length). So \(a = c - b\): the distance from head to cycle start equals the distance from meeting point to cycle start (going forward around the cycle).
Hard: Reverse Linked List in Groups of K¶
- Problem: reverse every \(k\) consecutive nodes in a linked list.
def reverse_k_group(head, k):
# check if we have k nodes left
node = head
for _ in range(k):
if not node:
return head
node = node.next
# reverse k nodes
prev, curr = None, head
for _ in range(k):
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
# head is now the tail of the reversed group
# recursively process the rest
head.next = reverse_k_group(curr, k)
return prev # prev is the new head of this group
- Pitfall: the in-place reversal pattern (
prev, curr, nxt) is worth memorising. Draw it out: at each step, you pointcurr.nextbackward toprev, then advance all three pointers. Getting the order wrong corrupts the list.
Stacks¶
-
A stack is LIFO (Last In, First Out): the most recently added element is removed first. Think of a stack of plates.
-
Operations:
push(x)adds to the top,pop()removes from the top,peek()looks at the top without removing. All \(O(1)\). -
Stacks are the implicit structure behind recursion (the call stack), expression evaluation (converting infix to postfix), and undo operations (each action is pushed, undo pops the last one).
Easy: Valid Parentheses¶
-
Problem: given a string of brackets
()[]{}, determine if they are balanced. -
Pattern: push opening brackets onto the stack. When you see a closing bracket, check that the top of the stack is the matching opener.
def is_valid(s):
stack = []
matching = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in matching:
if not stack or stack[-1] != matching[char]:
return False
stack.pop()
else:
stack.append(char)
return len(stack) == 0
- Pitfall: forgetting
len(stack) == 0at the end. The string "(((" has no mismatches but is not valid because unclosed brackets remain.
Pattern: Monotonic Stack¶
-
A monotonic stack maintains elements in sorted order (either increasing or decreasing). When a new element would violate the order, you pop elements until the order is restored.
-
When to use: problems asking "for each element, find the next/previous greater/smaller element." The stack gives \(O(n)\) total because each element is pushed and popped at most once.
Medium: Daily Temperatures¶
-
Problem: given daily temperatures, for each day find how many days until a warmer temperature.
-
Pattern: use a stack of indices. When the current temperature is higher than the stack's top, pop and record the distance.
def daily_temperatures(temperatures):
n = len(temperatures)
result = [0] * n
stack = [] # stack of indices, temperatures in decreasing order
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
prev = stack.pop()
result[prev] = i - prev
stack.append(i)
return result
-
Each element is pushed once and popped at most once: \(O(n)\) total.
-
Pitfall: storing indices on the stack (not values). You need the index to compute the distance.
Hard: Largest Rectangle in Histogram¶
-
Problem: given an array of bar heights, find the area of the largest rectangle.
-
Pattern: for each bar, find how far left and right it can extend (i.e., the nearest shorter bar on each side). A monotonic increasing stack tracks this efficiently.
def largest_rectangle(heights):
stack = [] # stack of indices, heights in increasing order
max_area = 0
heights.append(0) # sentinel to flush the stack at the end
for i, h in enumerate(heights):
start = i
while stack and stack[-1][1] > h:
idx, height = stack.pop()
max_area = max(max_area, height * (i - idx))
start = idx # this bar can extend back to where the popped bar started
stack.append((start, h))
heights.pop() # remove sentinel
return max_area
-
Pitfall: the
start = idxline is subtle. When we pop a bar that is taller than the current bar, the current bar can extend backward to where the popped bar started (because all bars in between were at least as tall as the popped bar). Missing this line gives incorrect areas. -
Pitfall: the sentinel
heights.append(0)ensures all remaining bars in the stack are processed. Without it, bars that never encounter a shorter bar to their right are missed.
Queues¶
-
A queue is FIFO (First In, First Out): elements are added at the back and removed from the front. Think of a line at a shop.
-
A deque (double-ended queue) supports \(O(1)\) insertion and removal at both ends. Python's
collections.dequeis the standard implementation. -
Queues are the structure behind BFS (breadth-first search, chapter 14 file 4), task scheduling, and message passing.
Easy: Implement Queue Using Stacks¶
-
Problem: implement a queue using only two stacks.
-
Pattern: use one stack for pushing and one for popping. When the pop stack is empty, transfer all elements from the push stack (reversing the order).
class MyQueue:
def __init__(self):
self.push_stack = []
self.pop_stack = []
def push(self, x):
self.push_stack.append(x)
def pop(self):
if not self.pop_stack:
while self.push_stack:
self.pop_stack.append(self.push_stack.pop())
return self.pop_stack.pop()
def peek(self):
if not self.pop_stack:
while self.push_stack:
self.pop_stack.append(self.push_stack.pop())
return self.pop_stack[-1]
def empty(self):
return not self.push_stack and not self.pop_stack
- Amortised \(O(1)\) per operation: each element is moved between stacks at most once.
Priority Queues and Heaps¶
-
A priority queue returns the smallest (or largest) element first, regardless of insertion order. The standard implementation is a binary heap.
-
A min-heap is a complete binary tree where every parent is smaller than its children. The minimum is always at the root. Stored as an array: the children of node \(i\) are at positions \(2i + 1\) and \(2i + 2\).
| Operation | Time |
|---|---|
| Insert | \(O(\log n)\) |
| Get min | \(O(1)\) |
| Extract min | \(O(\log n)\) |
| Build heap from array | \(O(n)\) |
- Python's
heapqmodule provides a min-heap. For a max-heap, negate the values.
import heapq
# Min-heap
h = []
heapq.heappush(h, 5)
heapq.heappush(h, 2)
heapq.heappush(h, 8)
print(heapq.heappop(h)) # 2 (smallest)
# Max-heap trick: negate values
heapq.heappush(h, -10)
print(-heapq.heappop(h)) # 10 (largest)
Medium: Kth Largest Element in an Array¶
-
Problem: find the kth largest element.
-
Pattern: maintain a min-heap of size \(k\). The heap's root is the kth largest. If the heap has \(k\) elements and a new element is larger than the root, replace the root.
import heapq
def find_kth_largest(nums, k):
heap = nums[:k]
heapq.heapify(heap) # O(k)
for num in nums[k:]:
if num > heap[0]:
heapq.heapreplace(heap, num) # pop min, push num: O(log k)
return heap[0]
-
\(O(n \log k)\) time, \(O(k)\) space. Much better than sorting (\(O(n \log n)\)) when \(k \ll n\).
-
Pitfall: using a max-heap of size \(n\) and popping \(k\) times also works but is slower: \(O(n + k \log n)\). The min-heap of size \(k\) is the optimal approach.
Hard: Merge K Sorted Lists¶
-
Problem: merge \(k\) sorted linked lists into one sorted list.
-
Pattern: use a min-heap containing the head of each list. Pop the smallest, add it to the result, and push its next node onto the heap.
import heapq
def merge_k_lists(lists):
heap = []
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst.val, i, lst))
dummy = ListNode(0)
curr = dummy
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
-
\(O(n \log k)\) where \(n\) is the total number of nodes. The heap always has at most \(k\) elements.
-
Pitfall: the
i(index) in the heap tuple is a tiebreaker. Without it, Python tries to compareListNodeobjects when values are equal, which crashes becauseListNodedoes not support<. The index ensures a valid comparison.
Common Pitfalls Summary¶
| Pitfall | Example | Fix |
|---|---|---|
Null pointer on fast.next |
Cycle detection with while fast.next |
Check fast and fast.next |
| Not handling empty list | Reverse of None |
Add if not head guard |
| Stack underflow | Popping from empty stack | Check len(stack) > 0 or if stack |
| Forgetting sentinel | Histogram misses last bars | Append 0 to flush the stack |
| Missing tiebreaker in heap | Comparing uncomparable objects | Add index to heap tuple |
| Modifying list during iteration | Removing nodes while traversing | Use prev/curr pattern or dummy head |
Take-Home Problems (NeetCode)¶
Linked Lists¶
- Reverse Linked List — the fundamental in-place reversal
- Merge Two Sorted Lists — two-pointer merge
- Linked List Cycle — fast/slow pointer
- Reorder List — find middle + reverse + merge
- Remove Nth Node From End — two pointers with gap \(n\)
- LRU Cache — hash map + doubly linked list
Stacks¶
- Valid Parentheses — matching brackets
- Min Stack — track min at each level
- Evaluate Reverse Polish Notation — stack-based evaluation
- Daily Temperatures — monotonic decreasing stack
- Largest Rectangle in Histogram — monotonic increasing stack
- Car Fleet — stack with time-to-target
Heaps / Priority Queues¶
- Kth Largest Element in a Stream — min-heap of size \(k\)
- Last Stone Weight — max-heap simulation
- K Closest Points to Origin — min-heap by distance
- Task Scheduler — greedy with max-heap + cooldown
- Find Median from Data Stream — two heaps (max-heap for lower half, min-heap for upper half)