Python Algorithms & Data Structures track

Lesson 07 · Part 2: Core Data Structures

Heaps & Priority Queues

heapq, min/max heaps and Top-K problems

Part 2: Core Data Structures3 min readFree lesson

Introduction

A heap is a data structure that can insert and extract the smallest (or largest) element in O(log n). Although you can find the minimum value by simple sorting (O(n log n)), a heap is much more efficient in situations where elements are continuously added and removed.

When do we use a heap?

  • Maintaining Top-K elements in streaming data
  • Dijkstra's shortest path algorithm
  • Huffman encoding
  • Job scheduling (priority queue)
  • Real-time median calculation

Learning Objectives

  1. Understand the complete binary tree structure of a heap and the heap property.
  2. Use Python's heapq module's min-heap and implement a max-heap with the negative trick.
  3. Implement the Top-K pattern and heap sort to write O(n log k) algorithms.

Key Concepts

1. Heap Structure -- Complete Binary Tree

1          <- Root (Minimum Value)
       /   \
      3     2
     / \   / \
    7   5 4   6

Array Representation: [1, 3, 2, 7, 5, 4, 6]
Index:     0  1  2  3  4  5  6

Parent(i) = (i - 1) // 2
Left(i) = 2 * i + 1
Right(i) = 2 * i + 2

Min-Heap Property: Parent node value <= Child node value


2. heapq Operation Complexity

Operation Function Time Complexity Description
Insert heappush(h, x) O(log n) Add element to heap
Extract Min heappop(h) O(log n) Remove root and reconstruct
Peek Min h[0] O(1) Check root without removal
Heap Creation heapify(lst) O(n) Convert list to heap
Top K nlargest(k, h) O(n log k) K largest elements
Bottom K nsmallest(k, h) O(n log k) K smallest elements
push+pop heappushpop(h, x) O(log n) push then pop (optimized)

3. Min-Heap vs Max-Heap

import heapq

#Min-Heap (Basic)
min_h = []
heapq.heappush(min_h, 3)
heapq.heappush(min_h, 1)
heapq.heappush(min_h, 2)
print(heapq.heappop(min_h))   #1 (Minimum Value)

#Max-Heap -- Negative Conversion Trick
max_h = []
heapq.heappush(max_h, -3)
heapq.heappush(max_h, -1)
heapq.heappush(max_h, -2)
print(-heapq.heappop(max_h))  #3 (Maximum Value)

4. Top-K Pattern

Keep only the top K out of N total:

Method 1: heapq.nlargest(k, arr) -- O(n log k)
Method 2: maintain a min-heap of size k -- O(n log k)

Maintain a min-heap of size k:
  for num in arr:
      heappush(h, num)
      if len(h) > k:
heappop(h)    # remove the smallest
-> The remaining k in the heap are up to k

Examples

File Content Key Concept
src/01_min_heap.py heapq basics push/pop/heapify min-heap operations
src/02_max_heap.py Implement max-heap using negation negation trick
src/03_top_k.py Top-K Maximum/Minimum elements nlargest, nsmallest
src/04_heap_sort.py Heap Sort O(n log n) heapify + heappop

Common Mistakes

Mistake 1: Forgetting to restore negatives in Max-Heap

import heapq

h = []
for v in [3, 1, 4, 1, 5]:
    heapq.heappush(h, -v)

val = heapq.heappop(h)
print(val)    #-5 <- Sign not restored!
print(-val)   #5  <- Correct maximum value

Mistake 2: heappushpop vs heappush+heappop

import heapq

h = [1, 2, 3]
heapq.heapify(h)

#Inefficient -- O(log n) * 2 times
heapq.heappush(h, 0)
heapq.heappop(h)

#Efficient -- O(log n) 1 time (internally optimized)
heapq.heappushpop(h, 0)

Mistake 3: Cannot delete arbitrary elements from heap

import heapq

h = [1, 3, 2, 5, 4]
heapq.heapify(h)
#del h[2]   # Heap property destroyed! Direct deletion prohibited
#Alternative: lazy deletion (mark as deleted and skip when popping)

Mistake 4: Comparing second value when first value is equal in tuple heap

import heapq

h = []
heapq.heappush(h, (1, "b"))
heapq.heappush(h, (1, "a"))
print(heapq.heappop(h))   #(1, 'a') -- Compared in dictionary order
#Types that cannot be compared are used in the form of (priority, id, obj)

Mistake 5: heapify is in-place -- no return value

import heapq

lst = [3, 1, 4, 1, 5, 9]
h = heapq.heapify(lst)   #Return None!
print(h)   # None

#Correct usage
heapq.heapify(lst)
print(lst[0])   #1 (Minimum Value)

Summary

Concept Key Point
Min-Heap Root is minimum, Python heapq default
Max-Heap Negation trick (-x push, -pop return)
heapify List -> Heap O(n), in-place
Top-K nlargest/nsmallest O(n log k)
Heap Sort O(n log n), in-place possible

Try it yourself

  1. Check why heapify O(n) in src/01_min_heap.py is faster than O(n log n).
  2. Try maintaining Top-K in a streaming manner in src/03_top_k.py.
  3. Solve the assignment below.

Example files

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

src/01_min_heap.pyPython
#Topic: Min-Heap Basics -- heappush, heappop, heapify, peek
# Time  : push O(log n), pop O(log n), heapify O(n)
#Space: O(n) -- Heap size

import heapq


def demo_basic_operations() -> None:
    """heapq basic operations demo."""
    print("=" * 50)
    print("Min-Heap Basic Operations")
    print("=" * 50)

    h: list[int] = []

    # heappush -- O(log n)
    for val in [5, 3, 8, 1, 9, 2, 7]:
        heapq.heappush(h, val)
        print(f"push({val}) -> Heap root (minimum): {h[0]}, Heap: {h}")

    print()
    #heappop -- O(log n): Always returns the minimum value
    print("pop order (always minimum value):")
    snapshot = list(h)
    while snapshot:
        val = heapq.heappop(snapshot)
        print(f"  pop() -> {val}")


def demo_heapify() -> None:
    """heapify -- Convert a list into a heap in O(n)."""
    print()
    print("=" * 50)
    print("heapify -- O(n) in-place conversion")
    print("=" * 50)

    lst = [9, 5, 3, 7, 1, 8, 2, 6, 4]
    print("Before conversion:", lst)

    heapq.heapify(lst)   #In-place, no return value
    print("After conversion:", lst)
    print("Root (minimum):", lst[0])

    #Note: heapify returns None
    lst2 = [3, 1, 2]
    ret = heapq.heapify(lst2)
    print(f"\nheapify() return value: {ret}  (None -- no return value)")
    print("After converting lst2:", lst2)


def demo_peek() -> None:
    """peek -- View the heap root (without removing)."""
    print()
    print("=" * 50)
    print("peek -- O(1) check minimum value")
    print("=" * 50)

    h = [4, 7, 2, 9, 1, 5]
    heapq.heapify(h)
    print("Heap:", h)
    print("peek (h[0]):", h[0])   #Check the minimum value without removal
    print("No heap change:", h)


def demo_tuple_heap() -> None:
    """Tuple heap -- (priority, value) pattern."""
    print()
    print("=" * 50)
    print("Tuple heap -- Priority queue")
    print("=" * 50)

    #Task scheduling: (priority, task name)
    h: list[tuple[int, str]] = []
    tasks = [(3, "Low priority"), (1, "Urgent"), (2, "Normal"), (1, "Urgent2")]
    for priority, name in tasks:
        heapq.heappush(h, (priority, name))
        print(f"  push({priority}, {name!r})")

    print("\nProcessing order:")
    while h:
        priority, name = heapq.heappop(h)
        print(f"Priority {priority}: {name}")


if __name__ == "__main__":
    demo_basic_operations()
    demo_heapify()
    demo_peek()
    demo_tuple_heap()
src/02_max_heap.pyPython
#Topic: Max-Heap -- Negative Conversion Trick
# Time  : push O(log n), pop O(log n)
#Space: O(n) -- Heap size

import heapq


def max_heappush(h: list[int], val: int) -> None:
    """Adding an element to a Max-Heap -- convert to negative and push."""
    heapq.heappush(h, -val)


def max_heappop(h: list[int]) -> int:
    """Extracting the maximum value from a Max-Heap -- restore the sign after pop."""
    return -heapq.heappop(h)


def max_peek(h: list[int]) -> int:
    """Query the maximum value of a Max-Heap (without removal)."""
    return -h[0]


def demo_max_heap() -> None:
    """Max-Heap demo."""
    print("=" * 50)
    print("Max-Heap -- Negative Conversion Trick")
    print("=" * 50)

    h: list[int] = []
    values = [5, 3, 8, 1, 9, 2, 7]

    for val in values:
        max_heappush(h, val)
        print(f"push({val}) -> Maximum: {max_peek(h)}, Internal heap: {h}")

    print()
    print("Pop order (always maximum):")
    snapshot = list(h)
    while snapshot:
        val = max_heappop(snapshot)
        print(f"  pop() -> {val}")


def demo_max_heap_with_heapify() -> None:
    """Convert the existing list into a Max-Heap."""
    print()
    print("=" * 50)
    print("Max-Heap heapify")
    print("=" * 50)

    lst = [3, 1, 4, 1, 5, 9, 2, 6, 5]
    print("Original:", lst)

    #Convert all elements to negative and then heapify
    neg_lst = [-x for x in lst]
    heapq.heapify(neg_lst)
    print("Internal heap (negative):", neg_lst)
    print("Maximum:", -neg_lst[0])

    #Extract all elements (in descending order)
    sorted_desc = []
    while neg_lst:
        sorted_desc.append(-heapq.heappop(neg_lst))
    print("Descending sort:", sorted_desc)


def demo_kth_largest() -> None:
    """Find the K-th largest element -- Maintain a Min-Heap of size K."""
    print()
    print("=" * 50)
    print("k-th largest element -- Maintain min-heap of size K")
    print("=" * 50)

    def kth_largest(nums: list[int], k: int) -> int:
        """Time O(n log k), Space O(k)."""
        h: list[int] = []
        for num in nums:
            heapq.heappush(h, num)
            if len(h) > k:
                heapq.heappop(h)    #Remove the smallest element
        return h[0]   #Root of size k min-heap = k-th largest value

    test_cases = [
        ([3, 2, 1, 5, 6, 4], 2, 5),
        ([3, 2, 3, 1, 2, 4, 5, 5, 6], 4, 4),
    ]
    for nums, k, expected in test_cases:
        result = kth_largest(nums, k)
        ok = "OK" if result == expected else "NG"
        print(f"nums={nums}, k={k} -> {result}  {ok}")


if __name__ == "__main__":
    demo_max_heap()
    demo_max_heap_with_heapify()
    demo_kth_largest()
src/03_top_k.pyPython
#Topic: Top-K Patterns -- nlargest, nsmallest, Streaming Top-K
#Time: O(n log k) -- processing n elements, maintaining a heap of size k
#Space: O(k) -- heap size

import heapq


def top_k_largest_builtin(nums: list[int], k: int) -> list[int]:
    """Using heapq.nlargest -- O(n log k)."""
    return heapq.nlargest(k, nums)


def top_k_smallest_builtin(nums: list[int], k: int) -> list[int]:
    """Using heapq.nsmallest -- O(n log k)."""
    return heapq.nsmallest(k, nums)


def top_k_largest_manual(nums: list[int], k: int) -> list[int]:
    """Maintain Top-K maximum values with a size k min-heap -- O(n log k).

    Idea: If the root of the min-heap < new element, replace the root.
    The k elements remaining in the heap at the end are the k overall maximum values."""
    h: list[int] = []
    for num in nums:
        if len(h) < k:
            heapq.heappush(h, num)
        elif num > h[0]:          #Replace if larger than the current k-th
            heapq.heapreplace(h, num)  #heappop + heappush (optimization)
    return sorted(h, reverse=True)


def streaming_top_k(stream: list[int], k: int) -> list[int]:
    """Maintaining Top-K in streaming data in real-time.

    Keep the Top-K each time an element comes in.
    Time O(n log k), Space O(k)."""
    h: list[int] = []
    results = []
    for num in stream:
        heapq.heappush(h, num)
        if len(h) > k:
            heapq.heappop(h)
        results.append(sorted(h, reverse=True))
    return results[-1] if results else []


def top_k_frequent(nums: list[int], k: int) -> list[int]:
    """Return top-K elements based on frequency -- O(n log k)."""
    freq: dict[int, int] = {}
    for num in nums:
        freq[num] = freq.get(num, 0) + 1

    #nlargest with (frequency, value)
    return [num for num, _ in heapq.nlargest(k, freq.items(), key=lambda x: x[1])]


if __name__ == "__main__":
    nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
    k = 3

    print("=" * 55)
    print(f"Top-K Test (nums={nums}, k={k})")
    print("=" * 55)

    print("nlargest (top 3):", top_k_largest_builtin(nums, k))
    print("nsmallest (bottom 3):", top_k_smallest_builtin(nums, k))
    print("manual top-k largest:", top_k_largest_manual(nums, k))

    print()
    print("=" * 55)
    print("Streaming Top-K")
    print("=" * 55)
    stream = [7, 2, 5, 1, 9, 3]
    k2 = 3
    final = streaming_top_k(stream, k2)
    print(f"stream={stream}, k={k2}")
    print("Final Top-K:", final)

    print()
    print("=" * 55)
    print("Top-K by frequency")
    print("=" * 55)
    freq_cases = [
        ([1, 1, 1, 2, 2, 3], 2, [1, 2]),
        ([1], 1, [1]),
    ]
    for arr, k3, expected in freq_cases:
        result = top_k_frequent(arr, k3)
        ok = "OK" if sorted(result) == sorted(expected) else "NG"
        print(f"nums={arr}, k={k3} -> {result}  {ok}")

    print()
    print("=" * 55)
    print("nlargest vs sorted complexity comparison guide")
    print("=" * 55)
    print("nlargest(k, arr): O(n log k) -- advantageous when k << n")
    print("sorted(arr)[-k:]: O(n log n) -- similar when k is close to n")
    print("However, if k >= n/2, sorted may be faster")
src/04_heap_sort.pyPython
#Topic: Heap Sort -- O(n log n)
#Time: O(n log n) -- heapify O(n) + n times heappop O(log n)
#Space: O(n) heapq version / O(1) in-place version

import heapq


def heap_sort_asc(arr: list[int]) -> list[int]:
    """Heap sort ascending order -- using heapq, O(n log n).

    1. Convert list to a min-heap O(n)
    2. Repeat heappop n times -> ascending order O(n log n)"""
    h = list(arr)         #Copy (original preserved)
    heapq.heapify(h)      # O(n)
    result = []
    while h:
        result.append(heapq.heappop(h))   #O(log n) * n times
    return result


def heap_sort_desc(arr: list[int]) -> list[int]:
    """Heap sort descending order -- max-heap trick, O(n log n)."""
    h = [-x for x in arr]
    heapq.heapify(h)
    result = []
    while h:
        result.append(-heapq.heappop(h))
    return result


def heap_sort_inplace(arr: list[int]) -> None:
    """In-place heap sort -- Space O(1) (no additional array).

    Implement max heap directly for sorting.
    Python's heapq is a min-heap, so direct implementation is needed."""
    n = len(arr)

    def sift_down(arr: list[int], root: int, end: int) -> None:
        """Rearrange downward from the root position (max-heap)."""
        while True:
            largest = root
            left  = 2 * root + 1
            right = 2 * root + 2

            if left < end and arr[left] > arr[largest]:
                largest = left
            if right < end and arr[right] > arr[largest]:
                largest = right

            if largest == root:
                break
            arr[root], arr[largest] = arr[largest], arr[root]
            root = largest

    #1. Construct max-heap O(n)
    for i in range(n // 2 - 1, -1, -1):
        sift_down(arr, i, n)

    #2. Extract one by one and place at the end O(n log n)
    for end in range(n - 1, 0, -1):
        arr[0], arr[end] = arr[end], arr[0]  #Maximum value to the end
        sift_down(arr, 0, end)


if __name__ == "__main__":
    import random

    print("=" * 55)
    print("Heap Sort correctness test")
    print("=" * 55)

    test_cases = [
        [5, 3, 8, 1, 9, 2, 7, 4, 6],
        [1],
        [],
        [3, 3, 3],
        [9, 8, 7, 6, 5, 4, 3, 2, 1],
    ]

    for arr in test_cases:
        asc  = heap_sort_asc(arr)
        desc = heap_sort_desc(arr)

        #In-place verification
        inplace = list(arr)
        heap_sort_inplace(inplace)

        expected_asc  = sorted(arr)
        expected_desc = sorted(arr, reverse=True)

        ok_asc     = "OK" if asc     == expected_asc     else "NG"
        ok_desc    = "OK" if desc    == expected_desc    else "NG"
        ok_inplace = "OK" if inplace == expected_asc     else "NG"

        print(
            f"Input: {str(arr):30} |"
            f"Ascending order {ok_asc} |"
            f"Descending order {ok_desc} |"
            f"in-place {ok_inplace}"
        )

    print()
    print("=" * 55)
    print("Performance test (n=50,000)")
    print("=" * 55)
    import time

    big = random.sample(range(200_000), 50_000)

    t0 = time.perf_counter()
    heap_sort_asc(big)
    t1 = time.perf_counter()
    print(f"heap_sort_asc (heapq)  : {t1 - t0:.4f}s")

    t0 = time.perf_counter()
    sorted(big)
    t1 = time.perf_counter()
    print(f"sorted() (Timsort)     : {t1 - t0:.4f}s")
    print("(Timsort is faster built-in in CPython)")

Assignment

Pre-Submission Checklist

  • Did you import heapq?
  • Did you use the negative conversion trick when a Max-Heap is needed?
  • Have you indicated the time complexity in comments?

Problem 1: Baekjoon 1927 -- Minimum Heap (Silver II)

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

Problem Description

Among widely known data structures, there is a minimum heap. Write a program that supports the following operations using a minimum heap.

  • Insert a natural number x into the set.
  • Print the smallest value in the set and remove it. (If the set is empty, print 0)

Input/Output

9          (N)
0          -> 0 (empty heap)
12345678   -> push
1          -> push
2          -> push
0          -> 1
0          -> 2
0          -> 12345678
0          -> 0 (empty heap)
32         -> push
0          -> 32

Hint

  • Use heapq
  • If x == 0, perform a pop operation
  • Time O(N log N), Space O(N)

Problem 2: Baekjoon 11279 -- Maximum Heap (Silver II)

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

Problem Description

It is the same as the minimum heap, but implement a maximum heap. Print and remove the largest value.

Hint

  • Negative conversion trick: negate when pushing -x, restore sign when popping -
  • Time O(N log N), Space O(N)

Problem 3: Baekjoon 11286 -- Absolute Value Heap (Silver I)

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

Problem Description

Implement a heap that prints the value with the smallest absolute value. If absolute values are the same, print the negative number first.

Input/Output Example

18
1 -> push 1
-1 -> push -1
0 -> -1 (|−1|=1 < ... if equal, negative first)
0 -> 1
...

Hint

  • Insert the tuple (|x|, x) into the heap to automatically sort by absolute value
  • When absolute values are the same, negative comes first: (-1, -1) vs (1, 1) -> (-1, -1) < (1, 1)
  • Time O(N log N), Space O(N)

Answer File

  • boj_1927.py
  • boj_11279.py
  • boj_11286.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_11279.pyPython
#Baekjoon 11279 -- Maximum Heap
#Time: O(N log N) -- N times push/pop
#Space: O(N) -- Heap size
#
#Solution: Negative conversion trick.
#Store as -x when pushing, restore sign as -result when popping.

import sys
import heapq

input = sys.stdin.readline


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

    for _ in range(n):
        x = int(input())
        if x == 0:
            output.append(str(-heapq.heappop(h)) if h else "0")
        else:
            heapq.heappush(h, -x)   #Store as negative (max-heap trick)

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


solve()
boj_11286.pyPython
#Baekjoon 11286 -- Absolute Value Heap
#Time: O(N log N) -- N times push/pop
#Space: O(N) -- Heap size
#
#Solution: Store the tuple (|x|, x) in the heap.
#If |x| is the same, the smaller x value (negative number) comes first.
#Example: (-1, -1) vs (1, 1) -> the second element of (-1, -1) is -1 < 1 -> negative comes first

import sys
import heapq

input = sys.stdin.readline


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

    for _ in range(n):
        x = int(input())
        if x == 0:
            if h:
                _, val = heapq.heappop(h)
                output.append(str(val))
            else:
                output.append("0")
        else:
            heapq.heappush(h, (abs(x), x))

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


solve()
boj_1927.pyPython
#Baekjoon 1927 -- Minimum Heap
#Time: O(N log N) -- N times push/pop
#Space: O(N) -- Heap size
#
#Solution: Python heapq is a min-heap by default.
#If x == 0, pop (if the heap is empty, print 0), otherwise push.

import sys
import heapq

input = sys.stdin.readline


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

    for _ in range(n):
        x = int(input())
        if x == 0:
            output.append(str(heapq.heappop(h)) if h else "0")
        else:
            heapq.heappush(h, x)

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


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