본문 바로가기
🛠️Language/python

[python] 힙(heap)과 힙큐(heapq)란?

by haegomm 2023. 8. 11.

 힙(heap) 

  • 최댓값과 최솟값을 빠르게 찾기 위해 고안된 자료구조
  • 완전 이진트리
  • 힙 property
    • A가 B의 부모노드이면 A의 키값과 B의 키값 사이에는 대소 관계가 성립한다.

최소 힙

  • 부모 노드의 키값이 자식 노드의 키값보다 항상 작은 힙

최대 힙

  • 부모 노드의 키값이 자식 노드의 키값보다 항상 큰 힙

 

  • 힙에서는 가장 낮은(혹은 높은) 우선순위를 가지는 노드가 항상 루트에 오게 된다.
    • 이를 이용해 우선순위 큐와 같은 추상적 자료형을 구현할 수 있다.
  • 키 값의 대소 관계
    • 부모/자식 노드 ⭕
    • 형제노드 ❌
  • 시간복잡도
    • O(log n)

 최소힙(heapq 모듈을 사용하지 않고 구현) 

# 최소힙

# 힙 삽입 연산
def heap_push(item):
    heap.append(item)
    child = len(heap) - 1
    parent = child // 2

    # 루트 노드이거나 최소힙 조건을 만족하지 못하면 종료
    while parent > 0 and heap[parent] > heap[child]:
        heap[parent], heap[child] = heap[child], heap[parent]
        child = parent
        parent = child // 2

# 힙 삭제 연산
def heap_pop():
    # 루트 노드만 남은 경우 바로 반환
    if len(heap) <= 2:
        return heap.pop()

    item = heap[1]  # 루트 노드 백업
    heap[1] = heap.pop()  # 마지막 노드를 루트 노드로 이동
    parent = 1
    child = parent * 2

    while child < len(heap):  # 자식이 하나라도 있으면
        if child + 1 < len(heap) and heap[child] > heap[child + 1]:
            # 오른쪽 자식이 있고, 오른쪽 자식이 왼쪽 자식보다 작다면
            child += 1  # 비교 대상을 오른쪽 자식으로 갱신

        # 자식이 더 작으면 최소힙이 아니므로 노드 교환
        if heap[parent] > heap[child]:
            heap[parent], heap[child] = heap[child], heap[parent]
            parent = child
            child = parent * 2
        # 최소힙 만족하면 탈출
        else:
            break

    return item  # 백업했던 루트 노드 반환

heap = [0]
heap_push(2)
heap_push(5)
heap_push(7)
heap_push(3)
heap_push(4)
heap_push(6)
print(heap)

while len(heap) >= 2:
    print(heap_pop())

 최대힙(heapq 모듈을 사용하지 않고 구현) 

# 최대힙

# 힙 삽입 연산
def heap_push(item):
    heap.append(item)
    child = len(heap) - 1
    parent = child // 2

    # 루트 노드이거나 최대힙 조건을 만족하지 못하면 종료
    while parent > 0 and heap[parent] < heap[child]:
        heap[parent], heap[child] = heap[child], heap[parent]
        child = parent
        parent = child // 2

# 힙 삭제 연산
def heap_pop():
    # 루트 노드만 남은 경우 바로 반환
    if len(heap) <= 2:
        return heap.pop()

    item = heap[1]  # 루트 노드 백업
    heap[1] = heap.pop()  # 마지막 노드를 루트 노드로 이동
    parent = 1
    child = parent * 2

    while child < len(heap):  # 자식이 하나라도 있으면
        if child + 1 < len(heap) and heap[child] < heap[child + 1]:
            # 오른쪽 자식이 있고, 오른쪽 자식이 왼쪽 자식보다 크다면
            child += 1  # 비교 대상을 오른쪽 자식으로 갱신

        # 자식이 더 크면 최대힙이 아니므로 노드 교환
        if heap[parent] < heap[child]:
            heap[parent], heap[child] = heap[child], heap[parent]
            parent = child
            child = parent * 2
        # 최대힙 만족하면 탈출
        else:
            break

    return item  # 백업했던 루트 노드 반환

heap = [0]
heap_push(2)
heap_push(5)
heap_push(7)
heap_push(3)
heap_push(4)
heap_push(6)
print(heap)

while len(heap) >= 2:
    print(heap_pop())

 


 힙큐(heapq) 

  • 파이썬 heapq 모듈은 heapq 알고리즘을 제공한다.
  • 이진 트리 기반의 최소 힙(min heap) 자료구조를 제공한다.
    • min heap을 사용하면 원소들이 항상 정렬된 상태로 추가되고 삭제된다.
    • min heap에서 가장 작은 값은 언제나 인덱스 0 즉, 이진 트리의 루트에 위치한다.
    • 내부적으로 min heap 내의 모든 원소(k)는 항상 자식 원소들 (2k+1, 2k+2)보다 크기가 작거나 같도록 원소가 추가되고 삭제된다.
heap[k] <= heap[2*k+1] and heap[k] <= heap[2*k+2]
     1  ---> root
   /   \\
  3     5
 / \\   /
4   8 7

 heapq 모듈을 이용한 최소힙 

from heapq import heappush, heappop

heap = []

heappush(heap, 2)
heappush(heap, 5)
heappush(heap, 7)
heappush(heap, 3)
heappush(heap, 4)
heappush(heap, 6)
print(heap)

while heap:
    print(heappop(heap))
  • heapq 모듈은 파이썬의 보통 리스트를 마치 최소 힙처럼 다룰 수 있도록 도와준다.
    • 파이썬에서는 heapq 모듈을 통해서 원소를 추가하거나 삭제한 리스트가 그냥 최소 힙이라는 것!

 heapq 모듈을 이용한 최대 힙 

from heapq import heappush, heappop

nums = [4, 1, 7, 3, 8, 5]
heap = []

for num in nums:
  heappush(heap, (-num, num))  # (우선 순위, 값)

while heap:
  print(heappop(heap)[1])  # index 1
8
7
5
4
3
1

 힙에 원소 추가 

heappush()

from heapq import heappush

heappush(heap, 4)
heappush(heap, 1)
heappush(heap, 7)
heappush(heap, 3)
print(heap)
[1, 3, 7, 4]
  • 가장 작은 1 이 인덱스 0에 위치한다.
  • 인덱스 1(=k)에 위치한 3 은 인덱스 3(=2k+1)에 위치한 4 보다 크므로 힙의 공식을 만족한다.

 힙에서 원소 삭제 

heappop()

from heapq import heappop

print(heappop(heap))
print(heap)
1
[3, 4, 7]
  • 가장 작았던 1 이 삭제되어 리턴되었고, 그 다음으로 작았던 3 이 인덱스 0이 되었다.

 최소값 삭제하지 않고 얻기 

  • 리스트의 첫번째 원소에 접근하듯이 인덱스를 통해서 접근하면 된다!
# [3, 4, 7]
print(heap[0]) # 3

⚠️ 주의사항

  • 힙(Heap)은 완전 이진 트리의 일종으로, 특별한 순서를 유지하는 배열이다.
  • 힙은 부모 노드가 자식 노드보다 작은 값을 갖도록 구성된 완전 이진 트리이다. 그러나 트리의 레벨별로 순서가 보장되어 있는 것은 아니다!
  • 인덱스 0에 가장 작은 원소가 있다고 해서 인덱스 1에 두 번째로 작은 원소, 인덱스 2에 세 번째로 작은 원소가 있다는 보장은 없다!
  • 힙은 heappop() 함수를 호출하여 원소를 삭제할 때마다 이진 트리의 재배치를 통해 매번 새로운 최솟값을 인덱스 0에 위치시킨다.
  • 따라서 두 번쨰로 작은 원소를 얻으려면 heap[1] 으로 접근하면 안되고, 반드시 heappop() 을 통해 가장 작은 원소를 삭제 후에 heap[0] 을 통해 새로운 최솟값에 접근해야한다.

 기존 리스트를 힙으로 변환 

heapify()

from heapq import heapify

heap = [4, 1, 7, 3, 8, 5]
heapify(heap)
print(heap)
[1, 3, 5, 4, 8, 7]

 

  • heapify() 는 새로운 리스트를 반환하는 것이 아니라 인자로 넘긴 리스트에 직접 변경한다.
    • 따라서 원본 리스트의 형태를 보존해야하는 경우에는 반드시 해당 리스트를 복제한 후에 인자로 넘겨야 한다!
nums = [4, 1, 7, 3, 8, 5]

heap = nums[:]
heapify(heap)

print(nums)
print(heap)
[4, 1, 7, 3, 8, 5]
[1, 3, 5, 4, 8, 7]

 


 응용 

 n번째 최솟값/최댓값 

nsmallest()

  • 주어진 리스트에서 가장 작은 n 개의 값을 담은 리스트를 반환한다.
    • 반환된 리스트의 마지막 값이 n 번째 작은 값이 된다.
from heapq import nsmallest

nsmallest(3, [4, 1, 7, 3, 8, 5])[-1]

 

nlargest()

  • n 번재 최댓값을 구한다.
from heapq import nlargest

nlargest(3, [4, 1, 7, 3, 8, 5])[-1]

 힙 정렬 

from heapq import heappush, heappop

def heap_sort(nums):
  heap = []
  for num in nums:
    heappush(heap, num)

  sorted_nums = []
  while heap:
    sorted_nums.append(heappop(heap))
  return sorted_nums

print(heap_sort([4, 1, 7, 3, 8, 5]))
[1, 3, 4, 5, 7, 8]

 

 


참고자료

https://www.daleseo.com/python-heapq/

 

파이썬의 heapq 모듈로 힙 자료구조 사용하기

Engineering Blog by Dale Seo

www.daleseo.com

 

'🛠️Language > python' 카테고리의 다른 글

[python] list 삭제 메서드 차이 + 요소 추가  (0) 2023.06.30
[python] 데크(deque)란?  (0) 2023.06.29