[13일차 과제] 자료구조(힙)
2026. 6. 8. 22:38ㆍKDT/과제
더보기
1. 힙이란?
2. 힙의 동작 과정
- 삽입
- 삭제
- Heapify
- 루트 제거 과정
3. 시간 복잡도
4. 힙 구현
- insert()
- delete()
- print_heap()
- 최대 힙/ 최소 힙 둘 다 구현
1. 힙(Heap)이란?
1. 힙의 개념 및 특징
- 특정한 순서에 따라 정렬된 요소들을 저장하는 트리 기반의 자료구조
- 여러 개의 값들 중 최댓값이나 최솟값을 빠르게 찾기 위해 만들어진 자료구조
- 힙 트리에서는 중복된 값을 허용 (이진 탐색 트리에서는 중복값 허용 x)
- 우선 순위가 가장 높은 데이터(최댓값)가 루트에 위치
- 완전 이진 트리로 구현:
마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있고, 마지막 레벨은 왼쪽부터 채워져 있는 형태 - 부모-자식 노드 관계 :
- 최대 힙에서는 부모 노드가 항상 자식노드보다 크거나 같은 값
- 최소 힙에서는 부모 노드가 항상 자식 노드보다 작거나 같은 값
- 배열 기반으로 구현하는 이유
- 완전 이진 트리의 특성을 활용해 부모-자식 노드 간의 이동을 간단히 해결하기 위함
- 트리를 탐색할 필요 없이 인덱스 연산만으로 접근이 가능
- (왼쪽 자식 인덱스) = (부모 인덱스) x 2
- (오른쪽 자식 인덱스) = (부모 인덱스) x 2 + 1
- (부모 인덱스) = (자식 인덱스) / 2
- 일반 이진 트리와의 차이점
| 일반 이진 트리 | 힙 (완전 이진 트리) | |
| 정의 | 자식 노드를 최대 2개까지만 가지는 트리 | 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있고, 마지막 레벨은 왼쪽부터 채워져 있는 형태 |
| 노드 규칙 | 부모-자식 노드 간의 특별한 규칙이 없음 | 부모 노드는 항상 자식 노드보다 크거나 같음 (최대 힙) 또는 작거나 같음 (최소 힙) |
| 주요 목적 | 데이터의 계층적 표현 및 탐색 | 우선순위 큐 구현, 최댓값/최솟값 탐색 |
| 구현 방식 | 주로 포인터를 이용해 노드 연결하여 구현 | 중간에 빈 공간이 없어, 메모리 낭비가 없는 배열로 구현 |
3. 힙 종류

- 최대 힙(Max Heap)
- 최대 트리 : 각 노드의 키 값이 자식 노드가 있다면 그 자식의 키 값보다 크거나 같은 트리
- 최대 힙 : 최대 트리이면서 완전 이진 트리
- 최소 힙(Min Heap)
- 최소 트리 : 각 노드의 키 값이 자식노드가 있다며녀 그 자식의 키 값보다 작거나 같은 트리
- 최소 힙 : 최소 트리이면서 완전 이진 트리
- 그 외 : 이진 힙, 피보나치 힙, 이항 힙, Min-Max 힙
2. 힙의 동작과정
1. 삽입(Insert)
- 트리의 가장 마지막 자리에 새로운 원소를 삽입
- 새로 삽입한 노드와 부모 노드 값 비교 (Heapify Up)
- 최대 힙 : 부모 노드보다 값이 크면 위치를 교환
- 최소 힙 : 부모 노드보다 값이 작으면 위치를 교환
- 부모 노드와 비교하여 올바른 위치를 찾을 때까지 반복
2. 삭제(Delete)
- 힙의 삭제는 루트 노드에서만 삭제가 이루어짐
- 최대 힙일 경우, 최댓값을 삭제
- 최소 힙일 경우, 최솟값을 삭제
- 루트 노드를 삭제
- 트리의 가장 마지막 노드를 루트 노드로 옮김
- 루트 노드와 자식 노드 값을 비교 (Heapify Down)
- 최대 힙 : 두 자식 중 큰 값과 비교하여 자식이 더 크면 위치를 교환
- 최소 힙 : 두 자식 중 작은 값과 비교하여 자식이 더 작으면 위치를 교환
- 자식 노드와 비교하여 올바른 위치를 찾을 때까지 이 과정을 반복
| 최대 힙 삽입 | 최대 힙 삭제 |
![]() |
![]() |
3. Heapify
- 데이터나 배열을 힙 구조로 재배열하는 과정
- 일반 배열 상태에서 힙 성질(부모-자식 노드간 관계)을 만족하도록, 노드 위치를 아래로 내리며 질서를 잡는 과정
- 삽입 / 삭제 후 힙 조건을 만족하도록 하는 과정에서 사용되는 연산
- 삽입 후 -> Heapify Up (상향식)
- 삭제 후 -> Heapify Down (하향식)
4. 루트 제거 과정
- 힙에서 루트 노드의 값을 반환하고 제거하는 연산
- 최대 힙 : 최댓값 반환 및 제거
- 최소 힙 : 최솟값 반환 및 제거
- 삭제와 동일한 과정이지만, 반환 값을 활용하는 것에 초점
- 삭제 : 노드 제거를 위함, 반환 값 없음
- 루트 제거 : 값을 꺼내서 활용하기 위함, 루트 값 반환
- 루트 노드의 값을 저장 (반환하기 위해서)
- 트리의 가장 마지막 노드를 루트 자리로 이동
- 마지막 노드였던 자리 제거
- 루트와 자식 노드 비교 (Heapify Down)
- 최대 힙 : 두 자식 중 큰 값과 비교하여 자식이 더 크면 위치를 교환
- 최소 힙 : 두 자식 중 작은 값과 비교하여 자식이 더 작으면 위치를 교환
- 올바른 위치를 찾을 때까지 반복
- 저장해둔 루트 값을 반환
3. 시간 복잡도
| 연산 | 시간 복잡도 | 설명 |
| 최대/최소 조회 | O(1) | 루트 노드 참조만 하면 됨 |
| 삽입 | O(log n) | 원소를 맨 끝에 추가 후, 부모 노드와 비교하여 위로 올라감 (상향식) |
| 삭제 | O(log n) | 루트 노드를 삭제하고, 맨 마지막 노드를 루트로 올린 뒤, 자식 노드와 비교하며 내려감 (하향식) |
| Heapify | O(n) | n개의 데이터를 가진 배열을 힙으로 한 번에 구성 |
4. 힙 구현
# class MaxHeap 최대 힙 구현
- init 생성자 :
- 힙은 초기에 데이터를 담을 배열만 있으면 됨
- insert() :
- 삽입할 요소를 매개변수로 받아옴
- 배열에 값 삽입
- 방금 삽입한(=마지막) 원소의 인덱스 cur = (배열길이) - 1
- 삽입한 위치(cur)부터 heapify_up 수행
- heapify_up() :
- 인덱스가 0이면 루트이므로 return (끝)
- 현재 노드의 부모 인덱스 구하기
(힙 배열의 인덱스가 0부터 시작하기 때문에 cur // 2는 안됨) - 현재 노드 요소가 부모 노드 요소보다 크면 두 요소의 자리를 바꿈
- 삽입했던 요소가 parent 위치에 있다. 완벽히 heap 될 때까지 heapify_up 재귀호출
- delete() :
- 배열이 비어 있으면 return (끝)
- heapify_down() :
- 왼쪽, 오른쪽 자식 노드 인덱스
- 현재 왼쪽, 오른쪽 중 가장 큰 값의 인덱스를 담을 변수 (초기값 = cur)
- 자식이 배열 범위 안에 있을 때, 왼쪽(오른쪽) 자식이 더 크면 big 갱신
- cur이 가장 크면 힙 조건 만족하므로 종료
- 두 요소의 자리를 바꾸고 완벽히 heap 될 때까지 heapify_down 재귀호출
- get_max() :
- delete와 다르게 요소 삭제 없이 루트 값(=최댓값)만 반환
- 배열이 비어있으면 None 리턴
아니면 루트 노드 리턴
- print_heap() :
- 배열 전체 출력
# 1. MaxHeap 구현
class MaxHeap:
def __init__(self):
self.h_arr = [] # 힙은 초기에 데이터를 담을 배열만 있으면 됨
# 삽입
def insert(self, value): # 삽입할 요소 매개변수로 받아옴
self.h_arr.append(value) # 배열에 값 삽입
cur = len(self.h_arr) - 1 # 방금 삽입한(=마지막) 원소의 인덱스 cur = (배열길이) - 1
self.heapify_up(cur) # 삽입한 위치(cur)부터 heapify_up 수행
def heapify_up(self, cur):
if cur == 0: # 인덱스가 0이면 루트이므로 return (끝)
return
parent = (cur - 1) // 2 # 현재 노드의 부모 인덱스 구하기. 힙 배열의 인덱스가 0부터 시작 하기 때문에 cur // 2는 안됨
if self.h_arr[cur] > self.h_arr[parent]: # 현재 노드 요소가 부모 노드 요소보다 크면
self.h_arr[cur], self.h_arr[parent] = self.h_arr[parent], self.h_arr[cur] # 두 요소의 자리를 바꿈 (튜플로 처리하는 swap)
# i = self.h_arr[cur]
# self.h_arr[cur] = self.h_arr[parent]
# self.h_arr[parent] = i
self.heapify_up(parent) # 삽입했던 요소가 parent 위치에 있다. 완벽히 heap이 될 때까지 heapify_up 재귀호출
# 삭제
def delete(self):
if len(self.h_arr) == 0: # 배열이 비어 있으면 return (끝)
return
self.h_arr[0] = self.h_arr[-1] # 마지막 노드를 루트 노드로 이동 (-1 = len(self.h_arr) - 1)
self.h_arr.pop() # 마지막 노드 제거
self.heapify_down(0) # 루트에 대해 heapify_down 수행
def heapify_down(self, cur):
left = 2 * cur + 1 # 왼쪽 자식 노드 인덱스
right = 2 * cur + 2 # 오른쪽 자식 노드 인덱스
big = cur # 현재 왼쪽, 오른쪽 중 가장 큰 값의 인덱스 (초기값 = cur)
# 자식이 배열 범위 안에 있을 때만 비교
if left < len(self.h_arr) and self.h_arr[left] > self.h_arr[big]: # 왼쪽 자식이 더 크면 big 갱신
big = left
if right < len(self.h_arr) and self.h_arr[right] > self.h_arr[big]: # 오른쪽 자식이 더 크면 big 갱신
big = right
if big == cur: # cur이 가장 크면 힙 조건 만족 -> 종료
return
self.h_arr[big], self.h_arr[cur] = self.h_arr[cur], self.h_arr[big] # 두 요소의 자리를 바꿈
self.heapify_down(big) # 완벽히 heap이 될 때까지 heapify_down 수행
# 최댓값 조회 (삭제 없이 루트 값만 반환)
def get_max(self):
if len(self.h_arr) == 0: # 배열이 비어있으면 delete와 달리 값을 반환해야하므로
return None # None 리턴
return self.h_arr[0] # 루트 노드 = 최댓값
# heap 데이터 출력
def print_heap(self):
print(self.h_arr) # 배열 전체를 프린트
# 2. 최대 힙 생성
h = MaxHeap()
# 3. 삽입 및 조회
h.insert(5)
h.insert(10)
h.insert(3)
h.insert(15)
h.print_heap() # [15, 10, 3, 5]
# 4. 삭제 및 조회
h.delete()
h.print_heap() # [10, 5, 3]
# 최소 힙 구현
- 최대 힙을 활용해 최소 힙 구현
- >를 <로, big을 small로
# MinHeap 구현
class MinHeap:
def __init__(self):
self.h_arr = [] # 힙은 초기에 데이터를 담을 배열만 있으면 됨
# 삽입
def insert(self, value): # 삽입할 요소 매개변수로 받아옴
self.h_arr.append(value) # 배열에 값 삽입
cur = len(self.h_arr) - 1 # 방금 삽입한(=마지막) 원소의 인덱스 cur = (배열길이) - 1
self.heapify_up(cur) # 삽입한 위치(cur)부터 heapify_up 수행
def heapify_up(self, cur):
if cur == 0: # 인덱스가 0이면 루트이므로 return (끝)
return
parent = (cur - 1) // 2 # 현재 노드의 부모 인덱스 구하기. 힙 배열의 인덱스가 0부터 시작 하기 때문에 cur // 2는 안됨
if self.h_arr[cur] < self.h_arr[parent]: # 현재 노드 요소가 부모 노드 요소보다 작으면
self.h_arr[cur], self.h_arr[parent] = self.h_arr[parent], self.h_arr[cur] # 두 요소의 자리를 바꿈 (튜플로 처리하는 swap)
# i = self.h_arr[cur]
# self.h_arr[cur] = self.h_arr[parent]
# self.h_arr[parent] = i
self.heapify_up(parent) # 삽입했던 요소가 parent 위치에 있다. 완벽히 heap이 될 때까지 heapify_up 재귀호출
# 삭제
def delete(self):
if len(self.h_arr) == 0: # 배열이 비어 있으면 return (끝)
return
self.h_arr[0] = self.h_arr[-1] # 마지막 노드를 루트 노드로 이동 (-1 = len(self.h_arr) - 1)
self.h_arr.pop() # 마지막 노드 제거
self.heapify_down(0) # 루트에 대해 heapify_down 수행
def heapify_down(self, cur):
left = 2 * cur + 1 # 왼쪽 자식 노드 인덱스
right = 2 * cur + 2 # 오른쪽 자식 노드 인덱스
small = cur # 현재 왼쪽, 오른쪽 중 가장 작은 값의 인덱스 (초기값 = cur)
# 자식이 배열 범위 안에 있을 때만 비교
if left < len(self.h_arr) and self.h_arr[left] < self.h_arr[small]: # 왼쪽 자식이 더 크면 small 갱신
small = left
if right < len(self.h_arr) and self.h_arr[right] < self.h_arr[small]: # 오른쪽 자식이 더 크면 small 갱신
small = right
if small == cur: # cur이 가장 작으면 힙 조건 만족 -> 종료
return
self.h_arr[small], self.h_arr[cur] = self.h_arr[cur], self.h_arr[small] # 두 요소의 자리를 바꿈
self.heapify_down(small) # 완벽히 heap이 될 때까지 heapify_down 수행
# 최솟값 조회 (삭제 없이 루트 값만 반환)
def get_min(self):
if len(self.h_arr) == 0: # 배열이 비어있으면 delete와 달리 값을 반환해야하므로
return None # None 리턴
return self.h_arr[0] # 루트 노드 = 최솟값
# heap 데이터 출력
def print_heap(self):
print(self.h_arr) # 배열 전체를 프린트
* 힙을 구현할 때는 init 함수에 배열만 있으면 된다.
* 다른 언어와 달리 파이썬에서는 두 변수의 위치를 바꿀 때 튜플을 사용해 바꿔줄 수 있어 편리하다.
* 부모와 자식의 인덱스를 넣는 과정에서 틀리지 않도록 주의하자.
* 힙을 구현한 코드를 C언어로 본 적은 있어도 직접 구현한 적은 처음이라 해시테이블과 마찬가지로 시간이 오래 걸렸지만 뿌듯하다.
'KDT > 과제' 카테고리의 다른 글
| [알고리즘] 병합 정렬, 퀵 정렬 (with 재귀 함수) (0) | 2026.06.11 |
|---|---|
| [알고리즘] 버블 정렬, 선택 정렬, 삽입 정렬 (0) | 2026.06.10 |
| [12일차 과제] 시간복잡도, 공간복잡도 (0) | 2026.06.07 |
| [11일차 과제] 자료구조(해시 테이블) (0) | 2026.06.04 |
| [10일차 과제] 자료구조(트리) (0) | 2026.06.03 |

