heapq 모듈

  • Python의 heapq 모듈은 힙(heap) 자료 구조를 제공합니다.

  • heapq 모듈은 이진 힙(binary heap)으로 구현되어 있습니다.

  • Python에서 제공하는 heapq 모듈은 기본적으로 최소힙(min-heap)을 제공한다.

이진 힙(binary heap)이란?

min-heap은 이진 트리입니다. 정수배열[3, 5, 8, 9, 7] 를 heapify 하면 다음과 같이 요소들이 배치됩니다.

    3
   / \
  5   8
 / \
9   7
  • 3은 루트 노드가 되며, 가장 작은 값이며(기본 min-heap이기 때문에) index 0에 위치합니다.

  • 각 노드의 값은 하위 노드의 값보다 작거나 같습니다.

  • 인덱스 i 에 있는 요소의 자식은 인덱스 (2 * i + 1) 및 (2 * i + 2) 에 위치합니다.

min-heap 과 max-heap

Python에서 제공하는 heapq 모듈은 기본적으로 min-heap 입니다. max-heap을 만들려면 아래와 같은 코드로 만들 수 있습니다.

import heapq

# Initialize the heap
max_heap = []

# Push elements into the heap (negating the values)
heapq.heappush(max_heap, -9)
heapq.heappush(max_heap, -7)
heapq.heappush(max_heap, -5)
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -8)

# Heap as a list (with negated values)
print("Max-Heap as list (negated):", max_heap)  # Output: [-9, -8, -5, -3, -7]

# Pop the largest element (negated, then negate back to original)
largest = -heapq.heappop(max_heap)
print("Largest element:", largest)  # Output: 9
print("Max-Heap after pop (negated):", max_heap)  # Output: [-8, -7, -5, -3]

largest = -heapq.heappop(max_heap)
print("Largest element:", largest)  # Output: 8
print("Max-Heap after pop (negated):", max_heap)  # Output: [-7, -5, -3]

largest = -heapq.heappop(max_heap)
print("Largest element:", largest)  # Output: 7
print("Max-Heap after pop (negated):", max_heap)  # Output: [-5, -3]
  • min-heap은 항상 가장 작은 요소가 루트에 위차합니다.

  • 서브 노드에서 min-heap 규칙은 지켜지지 않을 수 있습니다. 왜냐하면 추가되는 부모 노드와 비교하여 값이 들어가기 때문입니다.

서브 노드에서 min-heap 규칙은 지켜지지 않을 수 있다는 예시는 아래와 같습니다.

# [3, 5, 8, 9, 7] 에 대한 min-heap

    3
   / \
  5   8
 / \
9   7
  • index 3, 즉 값 5의 자식 노드에서는 min-heap의 규칙이 지켜지지 않은 것을 확인할 수 있습니다.

  • 루트 노드 3, 그리고 그 하위 값 5 와 8이 순차적으로 부모 노드 3의 자식 노드로 추가됩니다.

  • 그 후 9라는 값이 들어갈 때 부모 노드 5 보다 9 값이 크기 때문에 그대로 부모 노드 5의 자식 노드로 추가되게 되는 것입니다.

그러면 위와 같은 상태에서 2라는 값을 넣으면 어떻게 될까요?

min-heap에 값을 채워넣을 때는, heap의 맨 끝, 즉 가장 마지막 자식 노드의 위치에 추가된 후, 부모 노드와 비교하여 필요한 위치로 이동하면서 힙의 특성을 유지합니다. 이 과정을 상향식 heap화라고 합니다.

  • 2라는 값은 부모 노드 8의 왼쪽 자식 노드로 들어가게 됩니다.

  • 채워진 값 2는 부모 노드의 값 8과 비교하여, 값이 더 작은 경우에 자리를 교체합니다.

  • 따라서, 원래 8은 2로, 2의 값은 8이 되어, 2가 부모 노드가 됩니다.

  • 그 후, 2라는 값은 부모 노드 3과 비교하고, 자리 교체 연산을 합니다.

  • 결국 2는 루트 노드에 위치하게 되며, 3은 2가 있던 자리, 즉 루트 노드의 오른쪽에 위치하게 됩니다.

최종 결과를 시각화한 모습입니다.

      2
    /   \
   5     3
  / \   /
 9   7 8

값을 출력해보면, [2, 5, 3, 9, 7, 8] 이 됩니다.

heap 의 데이터 삽입 및 제거에 대한 시간 복잡도는?

시간 복잡도는 O(logN) 으로 동일하게 유지됩니다.

dictionary 나 이중 배열 등에 대해서 heapq.heapify 함수를 실행한다면?

import heapq

data = {1: 'a', 2: 'b', 3: 'c'}
heapq.heapify(data)

# TypeError: heapify() argument must be list, not dict

data = {1, 3}
heapq.heapify(data)

data = (1,2,3,4)
heapq.heapify(data)
  • dictionary 는 에러가 발생한다.

  • set 는 에러가 발생한다.

  • tuple 은 에러가 발생한다.

  • 그 외, 정수와 문자열도 에러가 발생한다.

  • 이중 배열은 에러가 발생하지 않는다.

이중 배열을 heapify 하면 의도한 대로 동작하지 않을 수 있습니다.. heapq 모듈은 리스트의 요소들 간의 비교를 통해 heap을 구성하는데, 이중 배열의 경우 내부 리스트 간의 비교가 복잡해질 수 있기 때문에, 이중 배열을 heapify 하는 것은 고려하지 않는 것이 나을 수도 있습니다.

주요 함수

# item을 힙에 추가합니다.
heapq.heappush(heap, item)

# 힙에서 가장 작은 요소를 제거하고 반환합니다.
heapq.heappop(heap)

# 힙에서 가장 작은 요소를 제거하고 item을 추가합니다.
heapq.heapreplace(heap, item):

# 리스트 x를 힙으로 변환합니다.
heapq.heapify(x):

여기서 유의해야 할 점은 heapify 함수입니다. heapify는 함수는 주어진 리스트를 heap 자료구조로 변환 하는데, type 메서드로 자료형을 출력해보면 여전히 list 형인 것을 확인할 수 있습니다. 그러나 이는 잘못된 것이 아닙니다. heapq 모듈은 리스트를 heap처럼 취급하도록 하며 효율적으로 min-heap 연산을 수행할 수 있게 해주며, heapq 모듈에서 제공하는 연산은 기본적으로 리스트를 기반으로 동작합니다.

heapreplace 과 heappushpop 의 차이

  • heapreplace: 현재 가장 작은 값을 pop 하고 리턴한뒤, 새로운 값을 추가

  • heappushpop: 새로운 값을 추가한 뒤, 가장 작은 값을 pop 하여 리턴

만약 min-heap 에서 최대 값을 찾아야 한다면?

min-heap에서 최대 값을 추출하는 것은 min-heap의 특성상 바로 접근할 수 없기 때문에 직접적인 방법은 없지만, 아래의 몇 가지 방법으로 해결할 수 있습니다.

1. 힙 구조를 유지하면서 최대 값 찾기

min-heap 의 특성상 최대 값은 항상 자식 노드(리프 노드) 중 하나에 위치합니다. 따라서 자식 노드들 중에서 최대 값을 찾아야 합니다. 자식 노드는 힙 리스트의 대략 절반부터 끝까지의 요소들입니다. 예를 들어 길이가 n인 힙 리스트에서 자식 노드는 인덱스 n//2 부터 n - 1 까지의 요소들입니다.

import heapq


def find_max_in_min_heap(heap):
    # 힙의 절반 이후의 요소들 중 최대 값을 찾는다
    n = len(heap)
    max_value = max(heap[n//2:])
    return max_value


heap = [1, 3, 6, 5, 9, 8]
print(find_max_in_min_heap(heap))  # 출력: 9

2. 모든 요소를 힙에서 제거하면서 최대 값을 찾기

모든 요소를 힙에서 하나씩 제거하면서 최대 값을 찾을 수 있습니다. 하지만 이 방벙븐 힙을 파괴합니다.

import heapq


def extract_max_from_min_heap(heap):
    # 힙의 모든 요소를 제거하면서 최대 값을 찾는다
    max_value = float('-inf')
    while heap:
        value = heapq.heappop(heap)
        max_value = max(max_value, value)
    return max_value


heap = [1, 3, 6, 5, 9, 8]
heapq.heapify(heap)
print(extract_max_from_min_heap(heap))  # 출력: 9

3. 최대 힙을 사용하는 방법

최소 힙에서 최대 값을 찾는 대시, 최대 힙을 사용하면 최대 값을 쉽게 얻을 수 있습니다.

import heapq


class MaxHeap:
    def __init__(self):
        self.heap = []

    def push(self, item):
        heapq.heappush(self.heap, -item)

    def pop(self):
        return -heapq.heappop(self.heap)

    def max(self):
        return -self.heap[0] if self.heap else None


max_heap = MaxHeap()
for value in [1, 3, 6, 5, 9, 8]:
    max_heap.push(value)


print(max_heap.max())  # 출력: 9

heap 은 언제 사용되는가

  • 최소/최대 값을 자주 필요로 할 때

    • 힙은 최소/최대 값을 빠르게 찾고 제거할 수 있으므로, 이 기능이 필요한 알고리즘에 유용합니다.

  • 우선순위큐

    • 우선순위가 높은 요소를 먼저 처리해야 하는 경우 힙을 사용할 수 있습니다.

  • 정렬된 데이터 유지

    • 데이터 스트림에서 상위 k개의 요소를 유지하거나, 정렬된 데이터를 유지해야 하는 경우 힙을 사용합니다.

  • K번째 최소/최대 값

    • 대량의 데이터에서 상위 K개의 최소/최대 값을 찾아야 할 때 유용합니다.

Visualizing Operations of max-heap for [9, 7, 5, 3, 8]

Insert -9:

-9

Insert -7:

  -9
  /
-7

Insert -5:

  -9
  / \
-7  -5

Insert -3:

    -9
    / \
  -7  -5
  /
-3

Insert -8:

    -9        -9
    / \       / \
  -7  -5    -8  -5
  / \       / \
-3  -8    -3  -7

-8 값이 들어오면 해당 노드의 부모 -7과 자리륾 교체한다. -->

이상으로 Python 의 heapq 모듈에 대해서 알아봤습니다.

감사합니다.

Last updated