Stacks, Queues & Linked Lists
These three structures are simpler than trees and graphs, but they appear in interviews more often than most candidates expect. Stacks solve matching and nesting problems. Queues power BFS and order-preserving processing. Linked lists test pointer manipulation and careful edge-case handling. The problems are not conceptually difficult — they are mechanically tricky, and the difference between passing and failing is usually handling edge cases correctly.
Stacks
A stack is last-in, first-out (LIFO). Push adds to the top, pop removes from the top. Both operations are O(1).
Stack operations:
push(item) O(1) Add to top
pop() O(1) Remove and return top
peek()/top() O(1) View top without removing
is_empty() O(1) Check if empty
Matching & Nesting
The canonical stack problem is parentheses matching. Any problem involving nested or paired structures — brackets, tags, undo/redo — is a stack problem.
Problem: Determine if a string of brackets is valid.
Input: "({[]})" -> True
Input: "({[}])" -> False
Input: "(((" -> False
Approach:
matching = {')': '(', '}': '{', ']': '['}
stack = []
for char in s:
if char in '({[':
stack.push(char)
elif char in ')}]':
if stack is empty or stack.pop() != matching[char]:
return False
return stack is empty
Why a stack works: The most recently opened bracket must be the
first one closed. This is exactly LIFO order.
Monotonic Stack
A monotonic stack maintains elements in sorted order (either increasing or decreasing). It solves "next greater element" and "next smaller element" problems efficiently.
Problem: For each element in an array, find the next greater element.
Input: [4, 5, 2, 10, 8]
Output: [5, 10, 10, -1, -1]
Approach:
stack = [] # stores indices, maintains decreasing values
result = [-1] * len(arr)
for i in range(len(arr)):
while stack and arr[i] > arr[stack[-1]]:
idx = stack.pop()
result[idx] = arr[i]
stack.push(i)
How it works: When we encounter a value larger than the stack's
top, we have found the "next greater" for that top element.
We keep popping until the stack's top is no longer smaller.
Time: O(n) because each element is pushed and popped at most once.
Expression Evaluation
Stacks evaluate mathematical expressions, especially when converting from infix to postfix notation or evaluating postfix directly.
Problem: Evaluate a postfix (reverse Polish notation) expression.
Input: ["2", "3", "+", "4", "*"]
Meaning: (2 + 3) * 4 = 20
Approach:
stack = []
for token in tokens:
if token is a number:
stack.push(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+': stack.push(a + b)
if token == '-': stack.push(a - b)
if token == '*': stack.push(a * b)
if token == '/': stack.push(int(a / b))
return stack.pop()
Undo Operations
Any system with undo/redo is naturally modeled with stacks. The action stack records operations; undo pops the last action; redo pops from a second stack.
undo_stack = []
redo_stack = []
def perform(action):
undo_stack.push(action)
redo_stack.clear() # redo invalidated by new action
def undo():
action = undo_stack.pop()
redo_stack.push(action)
reverse(action)
def redo():
action = redo_stack.pop()
undo_stack.push(action)
replay(action)
Queues
A queue is first-in, first-out (FIFO). Enqueue adds to the back, dequeue removes from the front. Both operations are O(1).
Queue operations:
enqueue(item) O(1) Add to back
dequeue() O(1) Remove and return front
peek()/front() O(1) View front without removing
is_empty() O(1) Check if empty
BFS
The primary interview use of queues is BFS traversal. The queue ensures nodes are processed in order of their distance from the source.
Problem: Find the shortest path in a maze from start to end.
queue = [(start_row, start_col, 0)] # row, col, distance
visited = set()
visited.add((start_row, start_col))
while queue:
row, col, dist = queue.popleft()
if (row, col) == end:
return dist
for dr, dc in [(0,1), (0,-1), (1,0), (-1,0)]:
nr, nc = row + dr, col + dc
if is_valid(nr, nc) and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append((nr, nc, dist + 1))
return -1 # no path found
Order-Preserving Processing
Queues maintain processing order. When tasks must be handled in the order they arrive, a queue is the natural structure.
Problem: Given a stream of tasks with timestamps, process them
in arrival order.
task_queue = Queue()
def on_task_arrival(task):
task_queue.enqueue(task)
def process_next():
if not task_queue.is_empty():
task = task_queue.dequeue()
execute(task)
Deque (Double-Ended Queue)
A deque supports push and pop from both ends in O(1). It is useful when you need both stack and queue behavior or when you need a sliding window maximum/minimum.
Problem: Find the maximum in every sliding window of size k.
deque = [] # stores indices in decreasing order of values
result = []
for i in range(len(arr)):
# Remove elements outside the window
while deque and deque[0] <= i - k:
deque.popleft()
# Remove elements smaller than current (they can never be max)
while deque and arr[deque[-1]] < arr[i]:
deque.pop()
deque.append(i)
if i >= k - 1:
result.append(arr[deque[0]])
return result
Time: O(n). Each element enters and leaves the deque at most once.
Linked Lists
A linked list is a sequence of nodes where each node points to the next. The key advantage over arrays: O(1) insertion and deletion at any position (given a pointer to that position). The key disadvantage: no random access — finding the nth element requires O(n) traversal.
Linked list operations:
Access by index: O(n)
Search: O(n)
Insert at head: O(1)
Insert at position: O(1) if you have the pointer, O(n) to find it
Delete at position: O(1) if you have the pointer, O(n) to find it
Reversing a Linked List
The most common linked list interview problem. It tests pointer manipulation.
Reverse a singly linked list:
prev = None
curr = head
while curr is not None:
next_node = curr.next # save next
curr.next = prev # reverse the link
prev = curr # advance prev
curr = next_node # advance curr
return prev # new head
Trace through example [1 -> 2 -> 3]:
Step 1: prev=None, curr=1. 1.next=None. prev=1, curr=2.
Step 2: prev=1, curr=2. 2.next=1. prev=2, curr=3.
Step 3: prev=2, curr=3. 3.next=2. prev=3, curr=None.
Result: 3 -> 2 -> 1
If you cannot write this from memory, practice until you can. It appears in interviews constantly and serves as a building block for harder linked list problems.
Detecting a Cycle
Floyd's cycle detection (tortoise and hare): use two pointers, one moving twice as fast as the other. If there is a cycle, they will meet.
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True # cycle detected
return False # no cycle (fast reached the end)
Why it works: If there is a cycle, the fast pointer enters it first.
The slow pointer then enters. Because the fast pointer moves 2 steps
per iteration while slow moves 1, the gap between them decreases by 1
each iteration. They must eventually meet.
Finding the Middle
Same two-pointer technique: when fast reaches the end, slow is at the middle.
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
return slow # middle node
Merging Two Sorted Lists
def merge(l1, l2):
dummy = Node(0)
current = dummy
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
current.next = l1 or l2 # attach remaining
return dummy.next
The dummy node trick avoids special-casing the head of the result list.
This is a standard technique — use it whenever you are building a new
linked list.
When These Simple Structures Are the Right Tool
Use a stack when:
- Matching or nesting (brackets, tags, expressions)
- Most recent first processing (undo, back button)
- Next greater/smaller element problems
- DFS (iterative implementation)
Use a queue when:
- BFS traversal
- Order-preserving processing
- Scheduling (process in arrival order)
- Sliding window max/min (deque variant)
Use a linked list when:
- Frequent insertion/deletion at arbitrary positions
- Building an LRU cache (doubly linked list + hash map)
- Merging sorted sequences
- Problems that explicitly use linked lists (many interview
questions give you a linked list as input)
The Dummy Node Pattern
When building or modifying linked lists, a dummy (sentinel) node at the head simplifies code by eliminating special cases:
Without dummy node:
if head is None:
head = new_node
else:
... find the right position and insert ...
With dummy node:
dummy = Node(0)
dummy.next = head
... always the same logic, no special case for head ...
return dummy.next
Use the dummy node pattern by default in interviews. It prevents bugs and makes your code cleaner.
Common Pitfalls
- Null pointer errors in linked lists: The most common linked list bug. Always check
node is not Nonebefore accessingnode.nextornode.val. - Losing the reference to head: When modifying a linked list, keep a reference to the head (or use a dummy node). If you lose it, you lose the entire list.
- Forgetting to handle empty input: Empty stack, empty queue, empty list (null head). Always handle these cases first.
- Off-by-one in fast/slow pointers: For finding the middle of an even-length list, whether slow ends up at the first or second middle node depends on the exact loop condition. Trace through a 4-element example to verify.
- Stack overflow with recursive linked list operations: Reversing a linked list recursively works but uses O(n) stack space. For very long lists, prefer the iterative version.
- Using an array as a queue: Removing from the front of an array is O(n) because all elements shift. Use a proper queue (deque in Python, LinkedList in Java) for O(1) operations.
- Not recognizing stack problems: If the problem involves "most recent" processing, nesting, or "undo," think stack immediately. Engineers often reach for complex solutions when a simple stack solves the problem.
Key Takeaways
- Stacks solve matching, nesting, and "most recent first" problems. If brackets, parentheses, or undo/redo appear in a problem, reach for a stack.
- Queues power BFS and any problem requiring FIFO processing. The deque variant handles sliding window max/min in O(n).
- Linked list problems test careful pointer manipulation. Practice reversing a list and detecting cycles until you can write them without thinking.
- The dummy node pattern eliminates edge cases in linked list problems. Use it by default.
- These structures are simple, but the problems they solve trip up candidates who overcomplicate things. Sometimes the right answer to a hard-looking problem is just a stack.