Python Algorithms & Data Structures track

Lesson 05 · Part 2: Core Data Structures

Stacks & Queues

list vs deque, bracket matching and postfix notation

Part 2: Core Data Structures4 min readFree lesson

Introduction

Stacks (Stack) and Queues (Queue) are the two major pillars of linear data structures. The two data structures have different data access orders, and this difference creates completely different algorithms.

  • Stack: LIFO (Last In, First Out) -- the last thing added is taken out first.
  • Real-world examples: stack of books, browser back button, function call stack, Ctrl+Z (undo)
  • Queue: FIFO (First In, First Out) -- the first thing added is taken out first.
  • Real-world examples: waiting in line, printer job queue, BFS search, OS process scheduling

In Baekjoon problems, stacks often appear for parenthesis checking and expression conversion, while queues frequently appear in BFS searches.


Learning Objectives

  1. Understand the operational principles and time complexity differences of a stack (list) and queue (deque).
  2. Implement a parenthesis matching algorithm using a stack and evaluate postfix expressions.
  3. Understand how In-BFS/DFS Queue/Stack-is are used and implement them yourself.

Key Concepts

1. Stack -- Python list

         PUSH ->  [  5  ]  <- TOP
                  [  3  ]
                  [  1  ]
                  +-----+
                   STACK

PUSH 5: [1, 3, 5]    TOP = 5
POP   : [1, 3]       Return 5
PEEK  : 3            Check TOP (do not remove)
Operation Python Time Complexity
push stack.append(x) O(1) average
pop stack.pop() O(1)
peek stack[-1] O(1)
isEmpty len(stack) == 0 O(1)

2. Queue -- collections.deque

ENQUEUE ->  [ front | 1 | 2 | 3 | 4 | back ]  <- ENQUEUE
DEQUEUE <-  [ front ]                         remove

list.pop(0)     -> O(n)  <- move all elements forward!
deque.popleft() -> O(1)  <- O(1) guaranteed at both ends

Why should we use deque?

Operation list deque
append(x) (right) O(1) O(1)
pop() (right) O(1) O(1)
insert(0, x) (left) O(n) O(1)
pop(0) (left) O(n) O(1)

Since the queue is dequeued from the left, you must use deque.


3. Parentheses Matching Algorithm

Input: { [ ( ) ] }

Processing steps:
'{' -> push      Stack: ['{']
'[' -> push      Stack: ['{', '[']
'(' -> push      Stack: ['{', '[', '(']
')' -> pop '('   Pair check: '(' == '(' OK   Stack: ['{', '[']
']' -> pop '['   Pair check: '[' == '[' OK   Stack: ['{']
'}' -> pop '{'   Pair check: '{' == '{' OK   Stack: []
Empty stack -> Valid parentheses!

4. Postfix / Reverse Polish Notation

Infix: 3 + 4 * 2 = 11
Postfix: 3 4 2 * +

Evaluation process (using stack):
Token 3 -> push  Stack: [3]
Token 4 -> push  Stack: [3, 4]
Token 2 -> push  Stack: [3, 4, 2]
Token * -> pop 4,2 -> 4*2=8 push  Stack: [3, 8]
Token + -> pop 3,8 -> 3+8=11 push Stack: [11]
Result: 11

Examples

File Content Key Concept
src/01_stack_basic.py Stack push/pop/peek Based on list, handles underflow
src/02_queue_basic.py Queue deque vs list BFS simulation, speed comparison
src/03_bracket_matching.py Parentheses matching ()[]{} Stack application
src/04_postfix_eval.py Postfix evaluation Operator handling

Viewing from a Different Perspective

Data Structure Analogy Feature
Stack Pile of books Only remove from the top
Queue Standing in line The person who comes first goes out first
Priority Queue Emergency room Process in order of importance
Deque Double-ended line Insert/Delete from both front and back

Common Mistakes

Mistake 1: Using list.pop(0) in a queue -- O(n)

#Wrong method -- O(n)
queue = [1, 2, 3]
queue.pop(0)   #Move all elements every time you take out from the front!

#Correct method -- O(1)
from collections import deque
queue = deque([1, 2, 3])
queue.popleft()   # O(1)

Mistake 2: Stack underflow not handled

stack = []
#pop without checking
val = stack.pop()   # IndexError!

#always check if it's empty
if stack:
    val = stack.pop()

Mistake 3: Missing empty stack check during parentheses matching

#Incorrect example
for ch in s:
    if ch in ")]}":
        top = stack.pop()   #IndexError if the stack is empty!

#Correct example
for ch in s:
    if ch in ")]}":
        if not stack:
            return False   #No matching opening bracket
        stack.pop()

Mistake 4: Accessing deque by index -- O(n)

from collections import deque
d = deque([1, 2, 3])
print(d[1])   #Possible, but O(n) -- the main purpose of a queue is not index access

Mistake 5: Reversing operand order in postfix notation

#Incorrect example: when calculating a - b
b = stack.pop()
a = stack.pop()
result = b - a   #Order reversed!

#Correct example
b = stack.pop()
a = stack.pop()
result = a - b   #The a that was pushed first is the left operand

Summary

Concept Key Point
Stack LIFO, O(1) push/pop with Python list
Queue FIFO, O(1) enqueue/dequeue using deque
Parenthesis Matching Push opening parenthesis, pop and check pair on closing parenthesis
Postfix Notation Push numbers, pop two for calculation when an operator appears
DFS Stack (recursive or explicit)
BFS Queue (deque)

Try it yourself

  1. Run src/01_stack_basic.py and manually check the DFS simulation results.
  2. Measure the actual speed difference between list and deque in src/02_queue_basic.py.
  3. Modify src/03_bracket_matching.py to support <> parentheses.
  4. Implement an infix -> postfix conversion function in src/04_postfix_eval.py.
  5. Solve the assignment below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/01_stack_basic.pyPython
#Topic: Stack Basics -- push, pop, peek, underflow, DFS Simulation
# Time  : push O(1), pop O(1), peek O(1)
#Space: O(n) -- Number of elements stored in the stack

from __future__ import annotations


class Stack:
    """Implementation of a list-based stack."""

    def __init__(self) -> None:
        self._data: list[int] = []

    def push(self, val: int) -> None:
        """Add an element to the top of the stack -- O(1) constant time."""
        self._data.append(val)

    def pop(self) -> int:
        """Remove and return the top element of the stack -- O(1).

        Raises:
            IndexError: When the stack is empty."""
        if self.is_empty():
            raise IndexError("Stack Underflow: Attempt to pop from an empty stack")
        return self._data.pop()

    def peek(self) -> int:
        """Returns the top element without removal -- O(1)."""
        if self.is_empty():
            raise IndexError("The stack is empty")
        return self._data[-1]

    def is_empty(self) -> bool:
        """Returns True if the stack is empty -- O(1)."""
        return len(self._data) == 0

    def size(self) -> int:
        """Returns the number of elements in the stack -- O(1)."""
        return len(self._data)

    def __repr__(self) -> str:
        return f"Stack(bottom->top): {self._data}"


def demo_basic_operations() -> None:
    """Stack basic operation demo."""
    print("=" * 50)
    print("Basic stack operations")
    print("=" * 50)

    s = Stack()
    for val in [1, 3, 5, 7, 9]:
        s.push(val)
        print(f"push({val:2}) -> {s}")

    print()
    while not s.is_empty():
        val = s.pop()
        print(f"pop() -> {val:2}  {s}")

    #Underflow Handling
    print()
    try:
        s.pop()
    except IndexError as e:
        print(f"Underflow Handling: {e}")


def dfs_iterative(graph: dict[int, list[int]], start: int) -> list[int]:
    """Iterative DFS using an explicit stack -- Time O(V+E), Space O(V).

    Implement DFS using a stack directly instead of recursion."""
    visited: list[int] = []
    seen: set[int] = set()
    stack: list[int] = [start]

    while stack:
        node = stack.pop()      #Stack LIFO -> DFS
        if node in seen:
            continue
        seen.add(node)
        visited.append(node)
        #Push neighbor nodes in reverse order (to maintain visit order consistency)
        for neighbor in reversed(graph.get(node, [])):
            if neighbor not in seen:
                stack.append(neighbor)

    return visited


def demo_dfs() -> None:
    """Graph DFS simulation."""
    print()
    print("=" * 50)
    print("Stack-based DFS Simulation")
    print("=" * 50)

    #   1
    #  / \\
    # 2   3
    # |   |
    # 4   5
    graph = {
        1: [2, 3],
        2: [4],
        3: [5],
        4: [],
        5: [],
    }
    print("Graph:", graph)
    order = dfs_iterative(graph, start=1)
    print("DFS Visit Order (Start=1):", order)   # [1, 2, 4, 3, 5]


if __name__ == "__main__":
    demo_basic_operations()
    demo_dfs()
src/02_queue_basic.pyPython
#Topic: Queue Basics -- Performance Comparison: deque vs list, BFS Simulation
#Time: enqueue O(1), dequeue O(1) (based on deque)
#Space: O(n) -- number of elements stored in the queue

import time
from collections import deque


def demo_deque_operations() -> None:
    """Deque basic operations demo."""
    print("=" * 50)
    print("basic deque operations")
    print("=" * 50)

    q: deque[int] = deque()

    #enqueue (add to the right)
    for val in [10, 20, 30, 40, 50]:
        q.append(val)
        print(f"enqueue({val:2}) -> front{list(q)}back")

    print()
    #dequeue (remove from the left)
    while q:
        val = q.popleft()
        print(f"dequeue() -> {val:2}  front{list(q)}back")

    #Deque: O(1) on both ends
    print()
    print("deque bidirectional operations:")
    d: deque[int] = deque([1, 2, 3])
    d.appendleft(0)   #add to the left
    d.append(4)       #add to the right
    print("appendleft(0), append(4):", list(d))   # [0,1,2,3,4]
    d.popleft()       #remove from the left
    d.pop()           #remove from the right
    print("popleft(), pop():         ", list(d))  # [1,2,3]

    #maxlen feature (useful for sliding window)
    window: deque[int] = deque(maxlen=3)
    for v in range(6):
        window.append(v)
        print(f"  append({v}) -> {list(window)}")


def benchmark_list_vs_deque(n: int = 100_000) -> None:
    """Performance comparison of list.pop(0) vs deque.popleft()."""
    print()
    print("=" * 50)
    print(f"Performance comparison: list.pop(0) vs deque.popleft() (n={n:,})")
    print("=" * 50)

    #list method -- O(n) per pop(0)
    lst = list(range(n))
    t0 = time.perf_counter()
    while lst:
        lst.pop(0)
    t1 = time.perf_counter()
    list_time = t1 - t0

    #deque method -- O(1) per popleft()
    dq = deque(range(n))
    t0 = time.perf_counter()
    while dq:
        dq.popleft()
    t1 = time.perf_counter()
    deque_time = t1 - t0

    print(f"list.pop(0)       : {list_time:.4f}s")
    print(f"deque.popleft()   : {deque_time:.4f}s")
    if deque_time > 0:
        print(f"deque is about {list_time / deque_time:.1f} times faster!")


def bfs_iterative(
    graph: dict[int, list[int]], start: int
) -> list[int]:
    """BFS using a queue -- Time O(V+E), Space O(V)."""
    visited: list[int] = []
    seen: set[int] = {start}
    queue: deque[int] = deque([start])

    while queue:
        node = queue.popleft()    # FIFO -> BFS
        visited.append(node)
        for neighbor in graph.get(node, []):
            if neighbor not in seen:
                seen.add(neighbor)
                queue.append(neighbor)

    return visited


def demo_bfs() -> None:
    """Graph BFS simulation."""
    print()
    print("=" * 50)
    print("Queue-based BFS simulation")
    print("=" * 50)

    #   1
    #  / \\
    # 2   3
    # |   |
    # 4   5
    graph = {1: [2, 3], 2: [4], 3: [5], 4: [], 5: []}
    print("Graph:", graph)
    order = bfs_iterative(graph, start=1)
    print("BFS visit order (start=1):", order)   # [1, 2, 3, 4, 5]


if __name__ == "__main__":
    demo_deque_operations()
    benchmark_list_vs_deque()
    demo_bfs()
src/03_bracket_matching.pyPython
#Topic : Bracket Matching -- (), [], {}
#Time  : O(n) -- string length n
#Space : O(n) -- in the worst case, n/2 stored in stack


def is_valid_brackets(s: str) -> bool:
    """Checks whether a bracket string is valid.

    Algorithm:
      - Push opening brackets ( [ { onto the stack
      - When a closing bracket ) ] } appears:
          1. If the stack is empty -> False (no matching pair)
          2. If it does not match the stack top -> False
          3. If it matches, stack.pop()
      - After all processing, if the stack is empty -> True"""
    match = {")": "(", "]": "[", "}": "{"}
    stack: list[str] = []

    for ch in s:
        if ch in "([{":
            stack.append(ch)
        elif ch in ")]}":
            if not stack or stack[-1] != match[ch]:
                return False
            stack.pop()

    return len(stack) == 0


def bracket_balance_detail(s: str) -> str:
    """Returns an explanation of what issue it is if it is invalid."""
    match = {")": "(", "]": "[", "}": "{"}
    stack: list[tuple[str, int]] = []   #(Bracket, Index)

    for i, ch in enumerate(s):
        if ch in "([{":
            stack.append((ch, i))
        elif ch in ")]}":
            if not stack:
                return f"Position {i}: No opening bracket corresponding to '{ch}'"
            top_ch, top_idx = stack[-1]
            if top_ch != match[ch]:
                return (
                    f"Position {i}: '{ch}' does not match '{top_ch}' at position {top_idx}"
                )
            stack.pop()

    if stack:
        ch, idx = stack[-1]
        return f"Position {idx}: No closing bracket corresponding to '{ch}'"
    return "Valid"


def count_min_removals(s: str) -> int:
    """Making valid parentheses with the minimum number of removals -- O(n)."""
    open_count = 0   #Number of '(' not matched
    close_count = 0  #Number of ')' not matched

    for ch in s:
        if ch == "(":
            open_count += 1
        elif ch == ")":
            if open_count > 0:
                open_count -= 1   #Match Success
            else:
                close_count += 1  #Match Failed ')'

    return open_count + close_count


if __name__ == "__main__":
    print("=" * 55)
    print("Bracket Matching Check")
    print("=" * 55)

    test_cases = [
        ("()",         True),
        ("()[]{}",     True),
        ("(]",         False),
        ("([)]",       False),
        ("{[]}",       True),
        ("",           True),
        ("(((", False),
        (")))", False),
        ("{[()]}",     True),
        ("([{}])",     True),
    ]

    print(f"{'Input':15} {'Expected':6} {'Result':6} {'Status'}")
    print("-" * 40)
    for s, expected in test_cases:
        result = is_valid_brackets(s)
        status = "OK" if result == expected else "NG"
        print(f"{s!r:15} {str(expected):6} {str(result):6} {status}")

    print()
    print("=" * 55)
    print("Detailed Error Analysis")
    print("=" * 55)
    detail_cases = ["(]", "([)]", "(((", "{[]}", ")))"]
    for s in detail_cases:
        print(f"{s!r:10} -> {bracket_balance_detail(s)}")

    print()
    print("=" * 55)
    print("Minimum Removal Count")
    print("=" * 55)
    removal_cases = ["()))(" , "((("]
    for s in removal_cases:
        print(f"{s!r:10} -> Minimum removals {count_min_removals(s)} times")
src/04_postfix_eval.pyPython
#Topic: Evaluation of Postfix Expression (Postfix / RPN) and Infix -> Postfix Conversion
#Time: O(n) -- proportional to the number of tokens
#Space: O(n) -- stack size

OPERATORS = set("+-*/^")
PRECEDENCE = {"+": 1, "-": 1, "*": 2, "/": 2, "^": 3}


def evaluate_postfix(expression: str) -> float:
    """Calculates a postfix expression -- Time O(n), Space O(n).

    Example: '3 4 2 * +' -> 3 + (4*2) = 11.0"""
    stack: list[float] = []
    tokens = expression.split()

    for token in tokens:
        if token in OPERATORS:
            if len(stack) < 2:
                raise ValueError(f"Not enough operands: stack={stack}, operator={token}")
            b = stack.pop()   #Right operand
            a = stack.pop()   #Left operand
            if token == "+":
                stack.append(a + b)
            elif token == "-":
                stack.append(a - b)
            elif token == "*":
                stack.append(a * b)
            elif token == "/":
                if b == 0:
                    raise ZeroDivisionError("Division by zero")
                stack.append(a / b)
            elif token == "^":
                stack.append(a ** b)
        else:
            stack.append(float(token))

    if len(stack) != 1:
        raise ValueError(f"Invalid expression: stack={stack}")
    return stack[0]


def infix_to_postfix(expression: str) -> str:
    """Convert infix expressions to postfix expressions -- Shunting-Yard algorithm.

    Time O(n), Space O(n)."""
    output: list[str] = []
    op_stack: list[str] = []
    tokens = expression.split()

    for token in tokens:
        if token not in OPERATORS and token not in "()":
            #Operand: output immediately
            output.append(token)
        elif token == "(":
            op_stack.append(token)
        elif token == ")":
            #Pop until matching '('
            while op_stack and op_stack[-1] != "(":
                output.append(op_stack.pop())
            if op_stack:
                op_stack.pop()  #Remove '('
        else:
            #Operator: compare precedence
            while (
                op_stack
                and op_stack[-1] != "("
                and op_stack[-1] in PRECEDENCE
                and PRECEDENCE[op_stack[-1]] >= PRECEDENCE[token]
            ):
                output.append(op_stack.pop())
            op_stack.append(token)

    #Output all remaining operators
    while op_stack:
        output.append(op_stack.pop())

    return " ".join(output)


if __name__ == "__main__":
    print("=" * 55)
    print("Postfix expression evaluation")
    print("=" * 55)

    postfix_cases = [
        ("3 4 +",                7.0),    # 3+4
        ("3 4 2 * +",           11.0),   # 3 + 4*2
        ("5 1 2 + 4 * + 3 -",  14.0),   # 5+((1+2)*4)-3
        ("2 3 ^",                8.0),   # 2^3
        ("10 2 /",               5.0),   # 10/2
    ]

    print(f"{'Postfix Expression':30} {'Expected':8} {'Result':8}")
    print("-" * 50)
    for expr, expected in postfix_cases:
        result = evaluate_postfix(expr)
        ok = "OK" if abs(result - expected) < 1e-9 else "NG"
        print(f"{expr:30} {expected:8.1f} {result:8.1f} {ok}")

    print()
    print("=" * 55)
    print("Infix -> Postfix Conversion (Shunting-Yard)")
    print("=" * 55)

    infix_cases = [
        "3 + 4",
        "3 + 4 * 2",
        "( 3 + 4 ) * 2",
        "5 + ( 1 + 2 ) * 4 - 3",
    ]

    for expr in infix_cases:
        postfix = infix_to_postfix(expr)
        value = evaluate_postfix(postfix)
        print(f"Infix: {expr:30} -> Postfix: {postfix:25} = {value}")

Assignment

Pre-Submission Checklist

  • Did you use list for stack and deque for queue?
  • Did you handle the case when popping from an empty In-stack/queue?
  • Have you indicated the time complexity in comments?

Problem 1: Baekjoon 9012 -- Parentheses (Silver IV)

Link: https://www.acmicpc.net/problem/9012

Problem Description

Given a parenthesis string (PS, Parenthesis String), we want to determine whether it is a valid parenthesis string. (), (()) are VPS (Valid Parenthesis String), but )(, (() are not VPS.

Input

6
(()())
(()((()))
(()([])
([
([)]
(()()

Output

YES
YES
NO
NO
NO
NO

Hint

  • In ( push and ), if the stack is empty, then NO, otherwise pop
  • After processing, if the stack is empty, then YES, otherwise NO
  • Time O(n), Space O(n)

Problem 2: Baekjoon 10845 -- Queue (Silver IV)

Link: https://www.acmicpc.net/problem/10845

Problem Description

Implement a queue that stores integers and write a program that processes the commands given as input.

List of commands: push X, pop, size, empty, front, back

Hint

  • Use of collections.deque is required
  • Use sys.stdin.readline for fast input
  • Time O(N), Space O(N)

Problem 3: Baekjoon 10828 -- Stack (Silver IV)

Link: https://www.acmicpc.net/problem/10828

Problem Description

Implement a stack that stores integers and write a program that processes the commands given as input.

List of commands: push X, pop, size, empty, top

Hint

  • Use Python list
  • Use sys.stdin.readline for fast input
  • Time O(N), Space O(N)

Answer File

  • boj_9012.py
  • boj_10845.py
  • boj_10828.py
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

boj_10828.pyPython
#Baekjoon 10828 -- Stack
#Time : O(N) -- Number of commands N
#Space: O(N) -- Stack size
#
#Solution: Implement the stack with a Python list and process according to the commands.

import sys

input = sys.stdin.readline


def solve() -> None:
    n = int(input())
    stack: list[int] = []
    output: list[str] = []

    for _ in range(n):
        cmd = input().split()
        op = cmd[0]

        if op == "push":
            stack.append(int(cmd[1]))
        elif op == "pop":
            output.append(str(stack.pop()) if stack else "-1")
        elif op == "size":
            output.append(str(len(stack)))
        elif op == "empty":
            output.append("1" if not stack else "0")
        elif op == "top":
            output.append(str(stack[-1]) if stack else "-1")

    print("\n".join(output))


solve()
boj_10845.pyPython
#Baekjoon 10845 -- Queue
#Time : O(N) -- Number of commands N
#Space: O(N) -- Queue size
#
#Solution: Implement a queue using collections.deque and process according to commands.

import sys
from collections import deque

input = sys.stdin.readline


def solve() -> None:
    n = int(input())
    queue: deque[int] = deque()
    output: list[str] = []

    for _ in range(n):
        cmd = input().split()
        op = cmd[0]

        if op == "push":
            queue.append(int(cmd[1]))
        elif op == "pop":
            output.append(str(queue.popleft()) if queue else "-1")
        elif op == "size":
            output.append(str(len(queue)))
        elif op == "empty":
            output.append("1" if not queue else "0")
        elif op == "front":
            output.append(str(queue[0]) if queue else "-1")
        elif op == "back":
            output.append(str(queue[-1]) if queue else "-1")

    print("\n".join(output))


solve()
boj_9012.pyPython
#Baekjoon 9012 -- Parentheses
#Time : O(N * K) -- N test cases, each string length K
#Space: O(K)    -- Stack size
#
#Solution: Push '(' , pop ')' (if the stack is empty, NO).
#If the stack is empty after processing all, YES.

import sys

input = sys.stdin.readline


def is_vps(s: str) -> str:
    stack: list[str] = []
    for ch in s:
        if ch == "(":
            stack.append(ch)
        else:  # ch == ')'
            if not stack:
                return "NO"
            stack.pop()
    return "YES" if not stack else "NO"


def solve() -> None:
    t = int(input())
    results = []
    for _ in range(t):
        s = input().strip()
        results.append(is_vps(s))
    print("\n".join(results))


solve()

Learn this lesson with a mentor

Work through the Python Algorithms & Data Structures track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now