힙은 우선순위 큐를 구현하기 위해서 사용되는 자료구조입니다.
우선순위 큐란?
우선순위가 가장 높은 데이터를 가장 먼저 추출하는 자료구조입니다. 데이터를 우선순위에 따라 처리하고 싶을 때 사용합니다.
예를 들어, 물건 데이터를 자료구조에 넣었다가 가치가 높은 물건부터 꺼내서 확인해야 되는 경우에 사용됩니다.
자료구조 | 추출되는 데이터 결과 |
스택 | 가장 나중에 삽입된 데이터 |
큐 | 가장 먼저 삽입된 데이터 |
우선순위 큐 | 가장 우선순위가 높은 데이터 |
우선순위 큐를 구현하는 방법
1. 단순히 리스트를 이용하여 구현합니다.
→ 차례대로 연달아 넣고, 리스트에서 데이터를 꺼낼 때 값들을 확인하고 가장 값이 큰 데이터부터 뽑아서 추출합니다.
2. 힙을 구현하여 구현합니다.
데이터의 갯수가 N개라고 가정하고 구현 방식에 따라서 시간복잡도를 비교하였을 때의 결과는 아래와 같습니다.
우선순위 큐 구현방식 | 삽입 시간 | 삭제 시간 |
리스트 | O(1) | O(N) |
힙(Heap) | O(logN) | O(logN) |
여기서, 우선순위 큐에서 단순히 N개의 데이터를 힙에 넣었다가 모두 꺼내는 작업은 정렬과 동일합니다.
→ 그 자체로써 정렬이 수행된다는 점이 특징입니다.
힙(Heap)이란?
우선순위 큐를 위하여 만들어진 자료구조입니다. 여러 개의 값들 중에서 최댓값이나 최솟값을 빠르게 찾아내도록 하는 자료구조입니다. 간단하게 말하자면, 부모 노드의 키 값이 자식 노드의 키 값보다 항상 큰(작은) 이진 트리를 말합니다.
→ 보통 최댓값, 최솟값을 찾는 연산을 빠르게 하기 위해서는 고안된 완전 이진트리를 기본으로 합니다.
힙(Heap)의 특징
1. 힙은 완전 이진 트리 자료구조의 일종입니다.
→ 이때 완전 이진 트리란, 루트 노드부터 시작하여 왼쪽 자식노드, 오른쪽 자식 노드 순서대로 데이터가 차례대로 삽입되는 트리를 의미합니다. 마지막 레벨을 제외하고 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 모든 노드는 가능한 한 가장 왼쪽에 있습니다.
2. 항상 루트 노드가 최댓값 또는 최솟값을 가지며, 이를 O(1) 시간 내에 접근할 수 있습니다. 삽입과 삭제의 시간 복잡도는 O(log n)으로 매우 효율적입니다.
3. 최소 힙(min heap)
- 루트 노드가 가장 작은 값을 가집니다.
- 따라서 값이 작은 데이터가 우선적으로 제거됩니다.
4. 최대 힙(max heap)
- 루트 노드가 가장 큰 값을 가집니다.
- 따라서 값이 큰 데이터가 우선적으로 제거됩니다.
5. 힙 트리에서는 중복된 값을 허용합니다. (이진 트리에서는 중복된 값을 허용하지 않습니다.)
힙(Heap)의 종류
1. 최소 힙 구성 함수 : Min-Heapify()
- 하나의 데이터가 들어왔을 때, 들어온 데이터로부터 부모 노드로 상향식으로 거슬러 올라가면서 매번 부모보다 자신의 값이 더 작은 경우에 위치를 교체할 수 있도록 합니다.
예를 들어, 새로운 데이터인 4가 들어왔을 때 최소 힙 성질을 만족하지 않으므로 교체하면서 최소 힙 함수를 구성할 수 있도록 합니다.
👀 여기서 최소 힙 Min-Heapify() 의 시간복잡도는?
기본적으로, 힙은 완전 이진트리 구조를 따르기 때문에 균형잡힌 트리로써 동작을 합니다. 그러므로, 항상 부모 쪽으로 거슬러 올라가거나 다시 내려올 때 최악의 경우에도 O(logN)의 시간 복잡도를 유지할 수 있습니다.
2. 최대 힙 구성 함수 : Max-Heapify()
- 하나의 데이터가 들어왔을 때, 들어온 데이터로부터 부모 노드로 상향식으로 거슬러 올라가면서 매번 부모노드보다 자신의 값이 더 큰 경우에 위치를 교체할 수 있도록 합니다.
힙(Heap)의 적용
- 힙에서 원소가 삽입될 때
힙에 새로운 요소가 들어오면, 일단 새로운 노드를 힙의 마지막 노드에 이어서 삽입합니다. 새로운 노드를 부모 노드들과 교환해서 힙의 성질을 만족시킵니다.
- 힙에서 원소가 삭제될 때
최대 힙에서 최댓값은 루트노드이므로 루트노드가 삭제됩니다. 삭제된 루트 노드에는 힙의 마지막 노드를 가져옵니다.
파이썬에서의 힙(Heap) 자료구조
파이썬 heapq 내장 모듈은 heapq 알고리즘을 제공합니다.
모든 부모 노드는 그의 자식 노드보다 값이 작거나 큰 이진트리 구조인데, 내부적으로는 인덱스 0에서 시작해 k번째 원소가 항상 자식 원소들(2k+1, 2k+2) 보다 작거나 같은 최소 힙의 형태로 정렬됩니다.
heapq는 내장 모듈로 별도의 설치 작업 없이 바로 사용할 수 있습니다.
힙(Heap) 생성 & 원소 추가
heapq 모듈은 리스트를 최소 힙처럼 다룰 수 있기 하기 때문에, 빈 리스트를 생성한 후 heapq의 함수를 호출할 때마다 리스트를 인자에 넘깁니다.
아래 예시를 통해서 설명하겠습니다.
1. heappush (원소 추가)
heappush 함수를 통해 item을 heap에 추가합니다.
- 호출 형태 : heapq.heappush(heap,item)
import heapq
heap=[]
heapq.heappush(heap,100)
heapq.heappush(heap,1)
heapq.heappush(heap,50)
heapq.heappush(heap,70)
print(heap)
→ 리스트를 최소 힙처럼 다룰 수 있도록 하기 때문에 heapq 함수를 호출할 때마다 리스트를 인자에 넘깁니다.
정확한 이해를 위해서 예를 통해 설명하겠습니다.
1. 4을 추가: 현재 힙은 [4]입니다.
- 트리 구조:
4
2. 1을 추가: 1이 4보다 작으므로 루트로 올라가고, 4는 1의 자식 노드로 이동합니다. 이때 힙은 [1, 4]가 됩니다.
- 트리 구조:
1
/
4
3. 3을 추가: 3은 4보다 작고, 1보다는 크므로 1의 자식 노드로 추가됩니다. 이때 힙은 [1, 4, 3]이 됩니다.
- 트리 구조:
1
/ \
4 3
4. 7을 추가: 7은 모든 기존 노드보다 크기 때문에 마지막에 추가되며, 트리 구조에서 왼쪽 자식 노드로 추가됩니다. 이때 힙은 [1, 4, 3, 7]이 됩니다.
1
/ \
4 3
/
7
2. heappop (원소 삭제)
heappop는 가장 작은 원소를 힙에서 제거함과 동시에 그를 결괏값으로 리턴합니다. 비어 있는 경우 IndexError가 호출됩니다.
- 호출 형태 : heapq.heappop(item)
result=heapq.heappop(heap)
print(result)
print(heap)
3. 힙 생성(변환)
heapify 함수를 통해 list를 heap으로 변환합니다.
- 호출 형태 : heapify(list)
import heapq
heap1=[70,30,100]
heapq.heapify(heap1)
print(heap1)
이처럼 파이썬에서는 기본적으로 min heap으로 구성되어있어서, 결과가 자연스럽게 오름차순으로 구성되는 것을 알 수 있습니다.
그러면 max heap(최대 힙)은 어떻게 구현해야 할까요?
최대 힙(Heap) 구현
이때 사용되는 개념은 아래와 같습니다.
💡 IDEA: y = -x 변환을 하면 최솟값 정렬이 최댓값 정렬로 바뀐다.
힙에 원소를 추가할 때 (-item, item)의 튜플 형태로 넣어주게 되면 튜플의 첫 번째 원소를 우선순위로 힙을 구성하게 됩니다. 이때 원소 값의 부호를 바꿨기 때문에, 최소 힙으로 구현된 heapq 모듈을 최대 힙 구현으로 바뀌게 되는 것입니다.
아래의 예시를 통해서 살펴보겠습니다.
import heapq
items = [1,3,5,7,9]
max_heap = []
for item in items:
heapq.heappush(max_heap, (-item, item))
for i in range(len(max_heap)):
print(max_heap[i][1],end=" ")
heappush 함수를 통해 item을 힙에 push 할 때 (-item, item)의 튜플의 형태로 넣은 것을 확인할 수 있습니다.
쉽게 말하자면, 최소 힙의 결과를 음수 형태로 변환하여서 최대 힙 형태로 출력하도록 하였기 때문에 실제로는 -item이 힙의 정렬 기준이 됩니다.
참고 자료
https://www.youtube.com/watch?v=AjFlp951nz0