Skip to content

Stacks and Queues

Stacks and queues are the same idea — “a sequence you only touch at the ends” — with one rule flipped. That single rule flip is the difference between undo history and a print queue.

What you’ll learn

  • Stack (LIFO) built from a plain listlist: appendappend/poppop.
  • Queue (FIFO) built from collections.dequecollections.deque: appendappend/popleftpopleft.
  • Why list.pop(0)list.pop(0) is O(n)O(n) and deque.popleft()deque.popleft() is O(1)O(1).
  • The monotonic stack pattern — Next Greater Element, runnable.
  • Using dequedeque as both a stack and a queue.
  • LeetCode-style problems to drill the pattern.

Stack: last in, first out

A stack only cares about one end. Python’s listlist is already perfect for this — appendappend and poppop (no argument) both operate on the end, both O(1)O(1) amortized.

stack_basics.py
stack = []
 
stack.append(1)
stack.append(2)
stack.append(3)
print("after pushes:", stack)
 
top = stack.pop()
print("popped:", top)
print("stack now:", stack)
 
print("peek (no pop):", stack[-1])
print("is empty?", len(stack) == 0)
stack_basics.py
stack = []
 
stack.append(1)
stack.append(2)
stack.append(3)
print("after pushes:", stack)
 
top = stack.pop()
print("popped:", top)
print("stack now:", stack)
 
print("peek (no pop):", stack[-1])
print("is empty?", len(stack) == 0)

Undo/redo, “match the brackets”, and depth-first traversal (both recursion and its explicit-stack equivalent) all lean on exactly this shape.

Queue: first in, first out

A queue needs cheap operations at both ends: push at the back, pop from the front. A plain listlist is the wrong tool here — pop(0)pop(0) has to shift every remaining element down by one, which is O(n)O(n).

why_not_list_pop0.py
# DON'T use a list as a queue if you pop from the front repeatedly.
queue_as_list = [1, 2, 3, 4, 5]
 
# Each pop(0) shifts every remaining element left by one slot -> O(n)
front = queue_as_list.pop(0)
print("popped from front:", front)
print("remaining:", queue_as_list, "  <- everything shifted, that cost O(n)")
why_not_list_pop0.py
# DON'T use a list as a queue if you pop from the front repeatedly.
queue_as_list = [1, 2, 3, 4, 5]
 
# Each pop(0) shifts every remaining element left by one slot -> O(n)
front = queue_as_list.pop(0)
print("popped from front:", front)
print("remaining:", queue_as_list, "  <- everything shifted, that cost O(n)")

collections.dequecollections.deque is implemented as a doubly linked sequence of fixed-size blocks, so both ends are cheap — no shifting, ever.

deque_queue.py
from collections import deque
 
queue = deque()
queue.append(1)   # enqueue: add at the back, O(1)
queue.append(2)
queue.append(3)
print("queue:", queue)
 
front = queue.popleft()   # dequeue: remove from the front, O(1)
print("dequeued:", front)
print("queue now:", queue)
deque_queue.py
from collections import deque
 
queue = deque()
queue.append(1)   # enqueue: add at the back, O(1)
queue.append(2)
queue.append(3)
print("queue:", queue)
 
front = queue.popleft()   # dequeue: remove from the front, O(1)
print("dequeued:", front)
print("queue now:", queue)

Monotonic stack: Next Greater Element

A monotonic stack keeps its elements in strictly increasing (or decreasing) order by popping anything that violates the order before pushing the new element. It’s the standard trick for “find the next bigger/smaller thing to the right” problems, and it does the whole array in one O(n)O(n) pass — each element is pushed and popped at most once.

next_greater_element.py
def next_greater_elements(nums):
    result = [-1] * len(nums)
    stack = []   # holds INDICES, kept so nums[stack] is decreasing
 
    for i, x in enumerate(nums):
        # pop every index whose value is smaller than the current one --
        # x IS their next greater element
        while stack and nums[stack[-1]] < x:
            j = stack.pop()
            result[j] = x
        stack.append(i)
 
    return result
 
 
nums = [2, 1, 2, 4, 3]
print(next_greater_elements(nums))
# 2 -> 4, 1 -> 2, 2 -> 4, 4 -> -1 (none), 3 -> -1 (none)
next_greater_element.py
def next_greater_elements(nums):
    result = [-1] * len(nums)
    stack = []   # holds INDICES, kept so nums[stack] is decreasing
 
    for i, x in enumerate(nums):
        # pop every index whose value is smaller than the current one --
        # x IS their next greater element
        while stack and nums[stack[-1]] < x:
            j = stack.pop()
            result[j] = x
        stack.append(i)
 
    return result
 
 
nums = [2, 1, 2, 4, 3]
print(next_greater_elements(nums))
# 2 -> 4, 1 -> 2, 2 -> 4, 4 -> -1 (none), 3 -> -1 (none)

Watch the stack grow and shrink as it scans the array — every pop happens because the current bar is taller than the one on top of the stack:

sketch Monotonic stack: Next Greater Element p5.js
Bars are scanned left to right. A gray outline marks the current index. Whenever the current bar is taller than the top of the stack (shown stacked on the right), that shorter bar is popped -- the current bar is its next greater element.

dequedeque as both a stack and a queue

collections.dequecollections.deque is O(1)O(1) at both ends, so it works perfectly as either a stack or a queue — one structure, two disciplines.

deque_both_roles.py
from collections import deque
 
# used as a STACK: push/pop the same end
stack = deque()
stack.append(1)
stack.append(2)
stack.append(3)
print("stack pop:", stack.pop())        # LIFO: 3
 
# used as a QUEUE: push at the back, pop from the front
queue = deque()
queue.append(1)
queue.append(2)
queue.append(3)
print("queue popleft:", queue.popleft())   # FIFO: 1
deque_both_roles.py
from collections import deque
 
# used as a STACK: push/pop the same end
stack = deque()
stack.append(1)
stack.append(2)
stack.append(3)
print("stack pop:", stack.pop())        # LIFO: 3
 
# used as a QUEUE: push at the back, pop from the front
queue = deque()
queue.append(1)
queue.append(2)
queue.append(3)
print("queue popleft:", queue.popleft())   # FIFO: 1

Complexity summary

StructureOperationComplexity
listlist as stackappendappend / pop()pop()O(1)O(1) amortized
listlist as queuepop(0)pop(0)O(n)O(n)
dequedeque as queueappendappend / popleft()popleft()O(1)O(1)
dequedeque as stackappendappend / pop()pop()O(1)O(1)
Monotonic stack scanwhole array, one passO(n)O(n) total

LeetCode problem set

#ProblemDifficultyThe twist
20Valid ParenthesesEasyThe definitive stack problem: push openers, and every closer must match the top
739Daily TemperaturesMediumA monotonic stack of indices, so the answer is a distance rather than a value
232Implement Queue using StacksEasyTwo stacks with amortised O(1)O(1) dequeue — only refill the out-stack when it empties

Practice — real LeetCode problems

Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.

LC 20 — Valid Parentheses · Easy

Problem. Given a string of ()[]{}()[]{}, decide whether the brackets are correctly closed in the correct order and matched by type.

Constraints. 1 <= len(s) <= 10^41 <= len(s) <= 10^4.

Examples. "()""()" gives TrueTrue · "()[]{}""()[]{}" gives TrueTrue · "(]""(]" gives FalseFalse · "([)]""([)]" gives FalseFalse

Editorial

The defining stack problem: the most recently opened bracket must be the first closed, which is LIFO by definition.

Time O(n)O(n). Space O(n)O(n) — worst case all openers.

Two distinct failure modes, both needed:

  • A closer with an empty stack"]""]". Popping without checking raises IndexErrorIndexError.
  • Leftovers at the end"(""(". The loop finishes happily, so the final emptiness check is the only thing that catches it.

And "([)]""([)]" is why a counter is insufficient: every bracket type is balanced in count, yet the nesting is wrong. Only a stack sees that.

Follow-ups: “One bracket type only?” — an integer counter suffices; it must never go negative and must end at zero. “Minimum additions to make it valid (LC 921)?” — count unmatched closers plus leftover openers. “Longest valid substring (LC 32)?” — much harder: push indices and measure gaps.

LC 1047 — Remove All Adjacent Duplicates In String · Easy

Problem. Repeatedly remove two adjacent equal letters until no such pair remains. Return the final string; the answer is unique.

Constraints. 1 <= len(s) <= 10^51 <= len(s) <= 10^5, lowercase letters.

Examples. "abbaca""abbaca" gives "ca""ca" · "azxxzy""azxxzy" gives "ay""ay"

Editorial

The naive reading suggests repeated passes until nothing changes, which is O(n2)O(n^2). A stack does it in one pass.

Time O(n)O(n). Space O(n)O(n).

The reason cascades work automatically: after popping a cancelled pair, the new top is whatever preceded them — and the next character is compared against that. So in "azxxzy""azxxzy", removing xxxx leaves zz on top, and the incoming zz cancels it immediately. No rescanning is needed because the stack already holds the reduced prefix.

"aaa""aaa" giving "a""a" is a good check: the first two cancel, and the third has nothing left to pair with.

Follow-ups: “Remove runs of kk adjacent duplicates (LC 1209)?” — push (char, count)(char, count) pairs and pop when the count reaches kk. “Is the result order-independent?” — yes, the problem states the answer is unique, and the stack argument shows why. “Do it in place?” — use the input list as the stack with a write pointer, giving O(1)O(1) extra space for a mutable sequence.

LC 682 — Baseball Game · Easy

Problem. Given a list of operations, compute the total score. Each entry is an integer (record it), "+""+" (record the sum of the previous two), "D""D" (record double the previous), or "C""C" (invalidate the previous record and remove it).

Constraints. 1 <= len(operations) <= 10001 <= len(operations) <= 1000, and every operation is valid when it appears.

Examples. ["5","2","C","D","+"]["5","2","C","D","+"] gives 3030 · ["5","-2","4","C","D","9","+","+"]["5","-2","4","C","D","9","+","+"] gives 2727

Editorial

A straightforward stack simulation, and a good illustration of why the undo operation ("C""C") is what makes a stack the right structure — a running total alone could not un-add a value it no longer remembers.

Time O(n)O(n). Space O(n)O(n).

The trap is "-2""-2". Dispatching with op.isdigit()op.isdigit() returns FalseFalse for a negative number, so it would fall through to an operator branch and either crash or corrupt the score. Testing for the three operator strings explicitly and treating everything else as a number is both simpler and correct. The same trap appears in LC 150 Evaluate RPN.

Follow-ups: “Support a redo?” — a second stack holding invalidated records. “What if "+""+" could appear with fewer than two records?” — the constraints guarantee validity; a defensive version would check len(stack) >= 2len(stack) >= 2. “Return the running score after each operation?” — keep a rolling total alongside the stack.

Recap

  • Stack = LIFO, built from a plain listlist (appendappend/poppop), both O(1)O(1) amortized.
  • Queue = FIFO. Never use list.pop(0)list.pop(0) (O(n)O(n)) — use collections.dequecollections.deque (appendappend/popleftpopleft, both O(1)O(1)).
  • A monotonic stack answers “next greater/smaller” queries for an entire array in one O(n)O(n) pass.
  • dequedeque is flexible enough to serve as either a stack or a queue.

Next: Hash Tables — how dictdict and setset get O(1)O(1) average lookup, and the collision handling underneath.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did