Stack: LIFO and Its Applications
A stack (Last In, First Out) is the go-to structure when you need to process or undo in reverse order: function call stack, expression evaluation, DFS, matching brackets, and “undo” operations. Key insight: whenever you see “the most recent unmatched X” or “process in reverse,” think stack. Python: use a list (append/pop). Java: Deque as stack (ArrayDeque). Do NOT use java.util.Stack — it is synchronized and slow.
Valid Parentheses (LC 20)
def is_valid(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for c in s:
if c in mapping:
top = stack.pop() if stack else '#'
if mapping[c] != top:
return False
else:
stack.append(c)
return not stack
Evaluate Reverse Polish Notation (LC 150)
def eval_rpn(tokens: list[str]) -> int:
stack = []
ops = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: int(a / b) # truncate toward zero
}
for t in tokens:
if t in ops:
b, a = stack.pop(), stack.pop()
stack.append(ops[t](a, b))
else:
stack.append(int(t))
return stack[0]
Min Stack (LC 155) — O(1) getMin
class MinStack:
def __init__(self):
self.stack = [] # (value, current_min) pairs
def push(self, val: int) -> None:
current_min = min(val, self.stack[-1][1]) if self.stack else val
self.stack.append((val, current_min))
def pop(self) -> None:
self.stack.pop()
def top(self) -> int:
return self.stack[-1][0]
def getMin(self) -> int:
return self.stack[-1][1]
LRU Cache (LC 146) — O(1) get and put
Use a doubly linked list (for O(1) insert and remove) combined with a hashmap (for O(1) lookup). Most-recently-used at the tail; least-recently-used at the head. On get: move to tail. On put: insert at tail; if over capacity, remove head.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.cache = OrderedDict() # maintains insertion order
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.cap:
self.cache.popitem(last=False) # remove LRU (first item)
Queue: BFS and Design
# Implement Queue using Two Stacks (LC 232)
class MyQueue:
def __init__(self):
self.inbox = [] # for push
self.outbox = [] # for pop/peek
def push(self, x: int) -> None:
self.inbox.append(x)
def _transfer(self):
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop())
def pop(self) -> int:
self._transfer()
return self.outbox.pop()
def peek(self) -> int:
self._transfer()
return self.outbox[-1]
def empty(self) -> bool:
return not self.inbox and not self.outbox
Amortized O(1) per operation: each element is pushed to inbox once, transferred to outbox once, and popped from outbox once — 3 operations total. The transfer only happens when outbox is empty.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”What is the O(1) trick for Min Stack and why does a separate min stack work?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The naive approach tracks the current min by scanning the entire stack on getMin() — O(n). The O(1) trick: store a (value, current_min) pair with every element pushed. The current_min at each level is min(new_value, previous_min). When you pop, the min at the level below is automatically restored because it was stored in that level's pair. A cleaner alternative: maintain a parallel min_stack. On push: if new value <= min_stack top (or min_stack is empty), push to min_stack too. On pop: if popped value == min_stack top, pop from min_stack too. getMin() returns min_stack top. Both approaches are O(1) amortized with O(n) extra space.”}},{“@type”:”Question”,”name”:”How does LRU Cache achieve O(1) for both get and put operations?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”LRU requires two operations: (1) Find a key instantly (O(1) lookup). (2) Move it to "most recently used" position and evict the least recently used in O(1). A hashmap alone gives O(1) lookup but O(n) for ordering. A doubly linked list gives O(1) insert/remove but O(n) lookup. Combined: hashmap maps keys to list nodes (O(1) lookup), doubly linked list maintains recency order (O(1) move-to-end and remove-from-front). On get: hashmap lookup finds the node, move it to the tail. On put: hashmap lookup checks if key exists (update or insert at tail), evict head if over capacity. Python's OrderedDict implements this pattern natively with move_to_end() and popitem(last=False).”}},{“@type”:”Question”,”name”:”Why does implementing a queue with two stacks give amortized O(1) per operation?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Each element is pushed to the inbox stack once (O(1)), transferred to the outbox stack once (O(1) amortized across all transfers), and popped from outbox once (O(1)). Total: 3 operations per element regardless of how many enqueue/dequeue operations are interleaved. The transfer (moving all elements from inbox to outbox) happens at most once per element — an element transferred to outbox stays there until popped, never going back to inbox. Worst case for a single dequeue: O(n) if transfer is triggered. But amortized over n operations: O(1) per operation. This is the standard amortized analysis — expensive operations are amortized over the cheap operations that precede them.”}},{“@type”:”Question”,”name”:”What is the difference between a stack and a deque, and when do you use each?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Stack: LIFO — add and remove only from one end (top). Deque (double-ended queue): add and remove from both ends in O(1). A deque is strictly more powerful than a stack. Use a deque when: sliding window maximum (add to right, remove from both ends based on value and window boundary), BFS with priority at both ends, or palindrome checking (read from both ends). Use a pure stack when the LIFO structure captures the problem naturally (bracket matching, DFS, undo). In Python: collections.deque supports appendleft(), popleft(), append(), pop() all in O(1). Java: ArrayDeque is the preferred implementation for both stack and deque use cases (LinkedList has higher memory overhead).”}},{“@type”:”Question”,”name”:”How do you evaluate a mathematical expression with operator precedence using a stack?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Shunting-yard algorithm (Dijkstra): converts infix expression (3 + 4 * 2) to postfix (3 4 2 * +), then evaluate postfix with a stack. Two stacks: output queue and operator stack. For each token: if number: push to output. If operator: pop operators from operator stack to output while they have higher or equal precedence. Push current operator to operator stack. If left paren: push to operator stack. If right paren: pop operators to output until left paren (discard both parens). At end: pop remaining operators to output. Evaluate postfix: for each token: if number, push. If operator, pop two numbers, apply operator, push result. Handles all standard mathematical precedence correctly in O(n).”}}]}
See also: Meta Interview Prep
See also: Atlassian Interview Prep
See also: Coinbase Interview Prep