0Pricing
Coding Interview Prep · 강의

Python heapq와 최대 힙 기법

heapq.heappush/heappop을 사용하고 값의 부호를 바꿔 최대 힙을 시뮬레이션하며, 빠른 상위 k개 질의에 heapq.nlargest/nsmallest를 적용합니다.

Python heapq와 최대 힙 기법은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

파이썬의 heapq 모듈 개요

파이썬의 heapq 모듈은 일반 파이썬 리스트 위에 구현된 최소 힙을 제공합니다. 전용 힙 클래스와 달리, heapq는 기존 리스트를 제자리에서 직접 조작합니다. 모듈 함수는 다음과 같습니다: O(n)에 힙을 만드는 heapify, O(log n)에 원소를 추가하는 heappush, O(log n)에 최솟값을 제거하는 heappop, 그리고 결합 연산의 효율을 높이는 heappushpop / heapreplace입니다.

import heapq

# heapq operates on plain Python lists
heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)
heapq.heappush(heap, 1)

print('Heap array:', heap)          # internal array (not sorted!)
print('Peek min:', heap[0])         # O(1) min access
print('Pop min:', heapq.heappop(heap))  # 1
print('Next min:', heap[0])         # 2

# heapify: turn any list into a heap in O(n)
data = [9, 4, 7, 1, 3, 6, 2]
heapq.heapify(data)
print('Heapified:', data, '| min:', data[0])

값을 음수로 바꾸어 구현하는 최대 힙

파이썬의 heapq는 최소 힙만 제공합니다. 최대 힙을 시뮬레이션하려면 삽입하기 전에 모든 값을 음수로 바꾸고, 꺼낼 때 다시 음수로 바꿉니다. 힙은 저장된 값을 기준으로 정렬하며, 음수로 바꾸면 정렬 순서가 반전되기 때문에 이 방법이 작동합니다. 양쪽에서 모두 음수로 바꾸어야 한다는 점을 항상 기억하세요. 삽입하기 전에는 음수로 바꾸고, 꺼낸 후에도 다시 음수로 바꿉니다. 어느 한 단계를 잊는 것은 면접에서 흔히 발생하는 버그입니다.

import heapq

max_heap = []
for val in [5, 1, 8, 3, 9, 2]:
    heapq.heappush(max_heap, -val)  # negate on push

print('Max-heap internal:', max_heap)  # all negated

# Pop in descending order:
results = []
while max_heap:
    results.append(-heapq.heappop(max_heap))  # negate on pop
print('Sorted descending:', results)  # [9, 8, 5, 3, 2, 1]

# Common pattern: top-k largest
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
k = 3
heap = []
for x in data:
    heapq.heappush(heap, -x)
print('Top', k, ':', [-heapq.heappop(heap) for _ in range(k)])

heapq.nlargest 및 nsmallest

heapq.nlargest(k, iterable)와 heapq.nsmallest(k, iterable)는 가장 크거나 작은 k개의 항목을 반환합니다. 이 함수들은 O(n log k)이므로 k가 n보다 훨씬 작을 때 전체 정렬(O(n log n))보다 효율적입니다. 내부적으로는 크기 k인 힙을 사용합니다. k가 n에 가까우면 파이썬은 전체 정렬로 전환합니다. 지속적으로 힙을 유지하지 않고 한 번만 상위 k개를 조회할 때 사용하세요.

import heapq

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

# Top 3 largest:
print(heapq.nlargest(3, data))   # [9, 8, 7]
# Top 3 smallest:
print(heapq.nsmallest(3, data))  # [1, 1, 2]

# With a key function:
words = ['banana', 'apple', 'cherry', 'date', 'elderberry']
print(heapq.nlargest(2, words, key=len))   # ['elderberry', 'banana']
print(heapq.nsmallest(2, words, key=len))  # ['date', 'apple']

# Note: when k ~ n, use sorted() instead:
# sorted(data)[-k:]  or  sorted(data, reverse=True)[:k]

복잡한 키를 위한 튜플 힙

힙 원소에 사용자 정의 비교 키가 필요할 때는 원소를 튜플 (priority, data)로 저장합니다. 파이썬의 heapq는 튜플을 요소별로 비교하므로 먼저 우선순위를 비교합니다. 우선순위가 같으면 두 번째 요소를 비교하는데, 데이터가 비교 가능하지 않으면 오류가 발생할 수 있습니다. 가장 안전한 패턴은 동률을 해결하기 위한 고유 카운터를 포함하여 데이터 요소를 직접 비교하는 일이 절대 없도록 하는 것입니다.

import heapq
import itertools

# Pattern: (priority, counter, item)
# Counter ensures unique tiebreaker, avoids comparing items
counter = itertools.count()
heap = []

def push_task(priority, task):
    heapq.heappush(heap, (priority, next(counter), task))

push_task(3, 'low priority task')
push_task(1, 'high priority task')
push_task(2, 'medium priority task')
push_task(1, 'another high priority')

while heap:
    pri, cnt, task = heapq.heappop(heap)
    print(f'P{pri}: {task}')
# Output in priority order: P1, P1, P2, P3

heapq.merge: 정렬된 반복 가능한 객체 병합

heapq.merge(*iterables)는 모든 데이터를 메모리에 적재하지 않고 여러 정렬된 반복 가능한 객체를 하나의 정렬된 출력으로 지연 병합합니다. 이는 크기 k인 최소 힙을 사용하는 K방향 병합과 같으며, 외부 정렬 알고리즘에서 사용됩니다. 반복자를 반환하므로 원소가 한 번에 하나씩 생성되며, 대규모 데이터 집합이나 스트리밍 상황에 적합합니다.

import heapq

# Merge multiple sorted lists efficiently
sorted_lists = [
    [1, 5, 9],
    [2, 6, 8],
    [3, 4, 7]
]

# heapq.merge takes sorted iterables and returns a merged sorted iterator
merged = list(heapq.merge(*sorted_lists))
print('Merged:', merged)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# The k-way merge manually (educational version):
def merge_k_sorted(lists):
    heap = []
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))
    result = []
    while heap:
        val, list_idx, elem_idx = heapq.heappop(heap)
        result.append(val)
        if elem_idx + 1 < len(lists[list_idx]):
            heapq.heappush(heap, (lists[list_idx][elem_idx+1], list_idx, elem_idx+1))
    return result

print('Manual k-way:', merge_k_sorted(sorted_lists))

힙의 지연 삭제 패턴

힙에서 임의의 원소를 제거해야 하지만 해당 원소의 인덱스를 모를 때는 지연 삭제를 사용하세요. 별도의 집합에 원소가 삭제되었다고 표시한 다음, 원소를 꺼낼 때 표시된 원소를 건너뜁니다. 이 방법은 상각 O(log n)이고 인덱스를 추적하는 복잡성을 피할 수 있습니다. 중복 항목을 사용하는 다익스트라 알고리즘과 작업 스케줄러 시뮬레이션에서 표준적으로 사용하는 방식입니다.

import heapq

class LazyHeap:
    def __init__(self):
        self._heap = []
        self._removed = set()

    def push(self, task):
        heapq.heappush(self._heap, task)

    def remove(self, task):
        self._removed.add(task)  # mark as removed

    def pop(self):
        while self._heap:
            task = heapq.heappop(self._heap)
            if task not in self._removed:
                return task
        return None

lh = LazyHeap()
for t in [5, 1, 8, 3, 2]:
    lh.push(t)
lh.remove(1)  # 'delete' 1 lazily
lh.remove(8)  # 'delete' 8 lazily
results = [lh.pop() for _ in range(3)]
print(results)  # [2, 3, 5] -- 1 and 8 skipped

스트림에서 K번째로 큰 원소

스트림에서 K번째로 큰 원소(LeetCode #703)는 크기 k인 최소 힙을 유지합니다. 힙의 루트는 지금까지 확인한 원소 중 항상 k번째로 큰 원소입니다. 새 숫자가 도착하면 힙에 삽입하고, 힙의 크기가 k를 초과하면 최솟값을 꺼냅니다. 힙에는 해당 원소보다 큰 원소가 정확히 k-1개 있으므로 루트는 항상 k번째로 큰 원소입니다.

import heapq

class KthLargest:
    def __init__(self, k, nums):
        self.k = k
        self.heap = []
        for num in nums:
            self.add(num)

    def add(self, val):
        heapq.heappush(self.heap, val)
        if len(self.heap) > self.k:
            heapq.heappop(self.heap)  # remove smallest
        return self.heap[0]  # kth largest = root of min-heap

# k=3, initial=[4,5,8,2]
kl = KthLargest(3, [4, 5, 8, 2])
print(kl.add(3))   # 4 (top 3: 8,5,4 -- kth=4)
print(kl.add(5))   # 5 (top 3: 8,5,5 -- kth=5)
print(kl.add(10))  # 5 (top 3: 10,8,5 -- kth=5)
print(kl.add(9))   # 8 (top 3: 10,9,8 -- kth=8)

합이 가장 작은 K개 쌍 찾기

합이 가장 작은 K개 쌍 찾기(LeetCode #373)는 최소 힙을 사용하여 쌍을 순서대로 생성합니다. 각 j에 대해 (nums1[0], nums2[j])인 모든 쌍으로 시작합니다. 최솟값을 꺼낸 다음, 꺼낸 쌍 (nums1[i], nums2[j])에 대해 같은 nums2 열에서 나오는 다음 후보인 (nums1[i+1], nums2[j])를 삽입합니다. 이는 힙을 사용하여 정렬된 쌍이나 곱을 생성할 때 자주 사용하는 패턴입니다.

import heapq

def k_smallest_pairs(nums1, nums2, k):
    if not nums1 or not nums2:
        return []
    heap = []
    # Initialize with pairs (nums1[0], nums2[j])
    for j in range(min(k, len(nums2))):
        heapq.heappush(heap, (nums1[0] + nums2[j], 0, j))
    result = []
    while heap and len(result) < k:
        total, i, j = heapq.heappop(heap)
        result.append([nums1[i], nums2[j]])
        if i + 1 < len(nums1):
            heapq.heappush(heap, (nums1[i+1] + nums2[j], i+1, j))
    return result

print(k_smallest_pairs([1,7,11], [2,4,6], 3))
# [[1,2], [1,4], [1,6]]

최대 힙을 사용하는 작업 스케줄러

작업 스케줄러(LeetCode #621)는 같은 작업 사이에 n개 간격의 냉각 기간을 두고 n개의 작업을 스케줄링하는 데 필요한 최소 시간을 구하는 문제입니다. 작업 빈도를 저장하는 최대 힙을 사용합니다. 각 시간 단계에서 실행 가능한 작업 중 빈도가 가장 높은 작업을 선택하고, 그 횟수를 줄인 다음 냉각 상태로 보냅니다. 한 주기마다 k=n+1개의 작업을 처리하거나, 부족한 부분을 유휴 시간으로 채웁니다. 최대 힙을 사용하는 이 탐욕적 접근법은 최적의 답을 제공합니다.

import heapq
from collections import Counter

def least_interval(tasks, n):
    freq = Counter(tasks)
    heap = [-f for f in freq.values()]  # max-heap (negated)
    heapq.heapify(heap)
    time = 0
    while heap:
        cycle = n + 1
        temp = []
        for _ in range(cycle):
            if heap:
                temp.append(heapq.heappop(heap))
        for f in temp:
            if f + 1 < 0:  # still tasks remaining
                heapq.heappush(heap, f + 1)
        # Add full cycle or remaining tasks if queue empty
        time += cycle if heap else len(temp)
    return time

print(least_interval(['A','A','A','B','B','B'], 2))  # 8
print(least_interval(['A','A','A','B','B','B'], 0))  # 6

다익스트라 알고리즘에서의 힙

다익스트라 알고리즘의 우선순위 큐는 최소 힙으로 구현합니다. 튜플 (distance, node)를 저장하고, 방문하지 않은 노드 중 가장 가까운 노드를 항상 먼저 처리합니다. 현재 알려진 최단 경로보다 더 큰 거리를 가진 노드를 꺼냈다면, 이는 지연 삭제로 인해 남은 오래된 항목이므로 건너뜁니다. 이렇게 하면 키 감소 연산이 필요 없고, O((V + E) log V) 복잡도를 유지하면서 구현을 간단하게 만들 수 있습니다.

import heapq

def dijkstra(graph, start):
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    heap = [(0, start)]  # (distance, node)
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:   # stale entry, skip
            continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist

graph = {
    'A': [('B', 4), ('C', 1)],
    'B': [('D', 1)],
    'C': [('B', 2), ('D', 5)],
    'D': []
}
print(dijkstra(graph, 'A'))  # {'A':0,'B':3,'C':1,'D':4}

최대 힙을 사용한 문자열 재구성

문자열 재구성(LeetCode #767)은 서로 인접한 두 문자가 같지 않도록 문자열을 재배열하는 문제입니다. (-frequency, char)로 구성된 최대 힙을 사용합니다. 각 단계에서 빈도가 가장 높은 문자를 꺼냅니다. 이전 문자가 현재 빈도가 가장 높은 문자와 같다면 두 번째로 빈도가 높은 문자를 대신 꺼냅니다. 이 탐욕적 접근법은 제약이 가장 큰 문자를 가능한 한 이른 시점에 배치하도록 합니다.

import heapq
from collections import Counter

def reorganize_string(s):
    freq = Counter(s)
    heap = [(-f, c) for c, f in freq.items()]
    heapq.heapify(heap)
    result = []
    prev_freq, prev_char = 0, ''
    while heap:
        freq, char = heapq.heappop(heap)
        result.append(char)
        # Push back the previous character if still remaining
        if prev_freq < 0:
            heapq.heappush(heap, (prev_freq, prev_char))
        prev_freq, prev_char = freq + 1, char  # decrement freq (less negative)
    result_str = ''.join(result)
    # Verify no adjacent duplicates
    return result_str if len(result_str) == len(s) else ''

print(reorganize_string('aab'))   # 'aba'
print(reorganize_string('aaab'))  # '' (impossible)

빠른 확인

이 단원에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 대비 개념에 대한 이해도를 테스트해 보세요.

학습 내용 요약

이 단원에서 배운 내용: 파이썬의 heapq 모듈 API와 heapify, heappush, heappop, nlargest, nsmallest, merge, 값을 음수로 바꾸어 구현하는 최대 힙 시뮬레이션, 그리고 상위 k개 스트리밍, 스트림에서 k번째로 큰 원소, 작업 스케줄러, 다익스트라를 포함한 일반적인 힙 면접 패턴입니다. 다음으로 데이터 스트림에서 중앙값을 구하는 방법과 K방향 병합을 살펴봅니다.

자주 묻는 질문

“Python heapq와 최대 힙 기법” 강의는 무료인가요?

네 — “Python heapq와 최대 힙 기법” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“Python heapq와 최대 힙 기법”에서 뭘 배우나요?

heapq.heappush/heappop을 사용하고 값의 부호를 바꿔 최대 힙을 시뮬레이션하며, 빠른 상위 k개 질의에 heapq.nlargest/nsmallest를 적용합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“Python heapq와 최대 힙 기법” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 힙 속성과 배열 표현
  2. 힙화, 삽입, 삭제를 처음부터 구현
  3. Python heapq와 최대 힙 기법
  4. 데이터 스트림의 중앙값과 K-way 병합
← Coding Interview Prep(으)로 돌아가기