개념

힙(Heap) - 완전 이진 트리 기반의 자료구조

부모와 자식 노드 간에 일정한 대소 관계를 유지


시간복잡도

연산시간복잡도
삽입O(log n)
삭제O(log n)
최댓값/최솟값 조회O(1)
힙 생성O(n)

특징

힙의 종류

  1. 최대 힙 (Max Heap)

    • 부모 노드 ≥ 자식 노드
    • 루트: 최댓값
  2. 최소 힙 (Min Heap)

    • 부모 노드 ≤ 자식 노드
    • 루트: 최솟값

힙의 성질

최대 힙 예시:
       100
      /   \
    19     36
   /  \   /
  17   3 25

최소 힙 예시:
        1
      /   \
     3     5
   /  \   /
  9   10 8

장점

  • 빠른 최댓값/최솟값 접근: O(1)
  • 효율적인 우선순위 관리: O(log n)
  • 완전 이진 트리: 배열로 효율적 구현

단점

  • 검색 비효율적: O(n)
  • 정렬된 상태 아님: 최상위 노드만 보장

Python 구현

1. heapq 모듈 (최소 힙)

import heapq
 
# 빈 힙 생성
heap = []
 
# 삽입
heapq.heappush(heap, 4)
heapq.heappush(heap, 1)
heapq.heappush(heap, 7)
heapq.heappush(heap, 3)
print(heap)  # [1, 3, 7, 4]
 
# 최솟값 삭제 및 반환
min_val = heapq.heappop(heap)
print(min_val)  # 1
print(heap)     # [3, 4, 7]
 
# 최솟값 조회 (삭제 X)
print(heap[0])  # 3
 
# 시간복잡도: O(log n)

2. 리스트를 힙으로 변환

import heapq
 
# 일반 리스트
arr = [4, 1, 7, 3, 8, 5]
 
# 힙으로 변환
heapq.heapify(arr)
print(arr)  # [1, 3, 5, 4, 8, 7]
 
# 시간복잡도: O(n)

3. 최대 힙 구현

import heapq
 
# 방법 1: 음수로 변환
max_heap = []
heapq.heappush(max_heap, -4)
heapq.heappush(max_heap, -1)
heapq.heappush(max_heap, -7)
 
max_val = -heapq.heappop(max_heap)
print(max_val)  # 7
 
# 방법 2: 튜플 활용 (우선순위, 값)
max_heap = []
heapq.heappush(max_heap, (-4, 4))
heapq.heappush(max_heap, (-1, 1))
heapq.heappush(max_heap, (-7, 7))
 
priority, val = heapq.heappop(max_heap)
print(val)  # 7

4. 힙 직접 구현

class MinHeap:
    def __init__(self):
        self.heap = []
 
    def push(self, val):
        self.heap.append(val)
        self._heapify_up(len(self.heap) - 1)
 
    def pop(self):
        if not self.heap:
            return None
 
        # 루트와 마지막 원소 교환
        self.heap[0], self.heap[-1] = self.heap[-1], self.heap[0]
        min_val = self.heap.pop()
 
        if self.heap:
            self._heapify_down(0)
 
        return min_val
 
    def _heapify_up(self, idx):
        parent = (idx - 1) // 2
 
        if idx > 0 and self.heap[idx] < self.heap[parent]:
            self.heap[idx], self.heap[parent] = self.heap[parent], self.heap[idx]
            self._heapify_up(parent)
 
    def _heapify_down(self, idx):
        smallest = idx
        left = 2 * idx + 1
        right = 2 * idx + 2
 
        if left < len(self.heap) and self.heap[left] < self.heap[smallest]:
            smallest = left
 
        if right < len(self.heap) and self.heap[right] < self.heap[smallest]:
            smallest = right
 
        if smallest != idx:
            self.heap[idx], self.heap[smallest] = self.heap[smallest], self.heap[idx]
            self._heapify_down(smallest)
 
# 테스트
heap = MinHeap()
heap.push(4)
heap.push(1)
heap.push(7)
print(heap.pop())  # 1

자주 나오는 문제 유형

1. K번째 큰 수 찾기

문제: 배열에서 K번째로 큰 수 찾기

import heapq
 
def find_kth_largest(nums, k):
    # 방법 1: 최소 힙 사용 (크기 k 유지)
    heap = nums[:k]
    heapq.heapify(heap)
 
    for num in nums[k:]:
        if num > heap[0]:
            heapq.heapreplace(heap, num)
 
    return heap[0]
 
# 테스트
print(find_kth_largest([3, 2, 1, 5, 6, 4], 2))  # 5
 
# 시간복잡도: O(n log k)
# 공간복잡도: O(k)

2. 배열을 힙으로 정렬

문제: 힙 정렬 구현

import heapq
 
def heap_sort(arr):
    # 최소 힙으로 정렬
    heapq.heapify(arr)
    return [heapq.heappop(arr) for _ in range(len(arr))]
 
# 내림차순 정렬
def heap_sort_desc(arr):
    # 음수로 변환하여 최대 힙처럼 사용
    arr = [-x for x in arr]
    heapq.heapify(arr)
    return [-heapq.heappop(arr) for _ in range(len(arr))]
 
# 테스트
print(heap_sort([3, 1, 4, 1, 5, 9, 2]))  # [1, 1, 2, 3, 4, 5, 9]
print(heap_sort_desc([3, 1, 4, 1, 5, 9, 2]))  # [9, 5, 4, 3, 2, 1, 1]
 
# 시간복잡도: O(n log n)

3. 두 개의 힙으로 중앙값 찾기

문제: 스트림에서 중앙값 실시간 계산

import heapq
 
class MedianFinder:
    def __init__(self):
        # 최대 힙 (작은 값들)
        self.small = []
        # 최소 힙 (큰 값들)
        self.large = []
 
    def add_num(self, num):
        # 최대 힙에 추가 (음수로 변환)
        heapq.heappush(self.small, -num)
 
        # 균형 맞추기
        if self.small and self.large and (-self.small[0] > self.large[0]):
            heapq.heappush(self.large, -heapq.heappop(self.small))
 
        # 크기 균형
        if len(self.small) > len(self.large) + 1:
            heapq.heappush(self.large, -heapq.heappop(self.small))
        if len(self.large) > len(self.small):
            heapq.heappush(self.small, -heapq.heappop(self.large))
 
    def find_median(self):
        if len(self.small) > len(self.large):
            return -self.small[0]
        return (-self.small[0] + self.large[0]) / 2
 
# 테스트
mf = MedianFinder()
mf.add_num(1)
mf.add_num(2)
print(mf.find_median())  # 1.5
mf.add_num(3)
print(mf.find_median())  # 2
 
# 시간복잡도: O(log n) - 삽입, O(1) - 조회

4. 합병 K개의 정렬된 리스트

문제: K개의 정렬된 연결 리스트를 하나로 합치기

import heapq
 
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
 
def merge_k_lists(lists):
    heap = []
 
    # 각 리스트의 첫 노드를 힙에 추가
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))
 
    dummy = ListNode()
    current = dummy
 
    while heap:
        val, i, node = heapq.heappop(heap)
        current.next = node
        current = current.next
 
        # 다음 노드를 힙에 추가
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
 
    return dummy.next
 
# 시간복잡도: O(N log k) - N: 전체 노드 수, k: 리스트 개수

5. 상위 K개 빈도 수

문제: 배열에서 가장 빈번한 K개의 원소 찾기

import heapq
from collections import Counter
 
def top_k_frequent(nums, k):
    # 빈도 계산
    count = Counter(nums)
 
    # 빈도를 음수로 최대 힙처럼 사용
    return heapq.nlargest(k, count.keys(), key=count.get)
 
# 테스트
print(top_k_frequent([1, 1, 1, 2, 2, 3], 2))  # [1, 2]
print(top_k_frequent([1], 1))  # [1]
 
# 시간복잡도: O(n log k)

추천 연습 문제

기초

중급

고급


핵심 요약

개념설명
완전 이진 트리 기반 자료구조
최소 힙부모 ≤ 자식 (루트: 최솟값)
최대 힙부모 ≥ 자식 (루트: 최댓값)
주요 연산삽입/삭제 O(log n), 조회 O(1)

heapq 주요 함수

import heapq
 
# 삽입
heapq.heappush(heap, item)
 
# 삭제 (최솟값)
heapq.heappop(heap)
 
# 조회 (최솟값)
heap[0]
 
# 리스트를 힙으로 변환
heapq.heapify(list)
 
# 삭제 후 삽입
heapq.heapreplace(heap, item)
 
# 삽입 후 삭제
heapq.heappushpop(heap, item)
 
# 최대 n개
heapq.nlargest(n, iterable, key=None)
 
# 최소 n개
heapq.nsmallest(n, iterable, key=None)

힙 vs 다른 자료구조

자료구조최댓값/최솟값삽입삭제검색
O(1)O(log n)O(log n)O(n)
배열 (정렬)O(1)O(n)O(n)O(log n)
BSTO(log n)O(log n)O(log n)O(log n)

활용 분야

1. 우선순위 큐

  • 작업 스케줄링
  • 이벤트 시뮬레이션

2. 정렬

  • 힙 정렬
  • 부분 정렬

3. 그래프 알고리즘

  • 다익스트라 최단 경로
  • 프림 MST

4. 스트림 데이터

  • 실시간 중앙값
  • 상위 K개 찾기

주의사항

1. Python heapq는 최소 힙만 지원

# 최대 힙이 필요하면 음수 변환
heapq.heappush(heap, -value)
max_val = -heapq.heappop(heap)

2. 힙은 부분적으로만 정렬됨

# heap[0]만 최솟값 보장
# 나머지는 정렬 X

3. 동일 우선순위 처리

# 튜플로 우선순위 추가
heapq.heappush(heap, (priority, count, item))

4. 배열 인덱스

# 부모: (i - 1) // 2
# 왼쪽 자식: 2 * i + 1
# 오른쪽 자식: 2 * i + 2