"힙은 종이 위에서는 트리지만 메모리에서는 배열이야. 노드 객체도 없고 left/right 포인터도 없어. 그냥 산술이지. '트리'가 실제로 쓸 수 있는 가장 빠른 구조 축에 들 수 있는 비밀이 여기 있어."
완전성이 배열을 사줘
이진 힙은 언제나 complete 이진 트리야. 마지막 레벨을 뺀 모든 레벨이 꽉 차 있고 마지막 레벨은 왼쪽부터 차례대로 채워지지. 이 모양에는 빈틈이 없어. 빈틈이 없는 트리는 평평한 배열에 레벨 순서대로 딱 맞아떨어져서 낭비되는 슬롯이 하나도 없고. 그래서 힙에는 노드 객체도 포인터도 전혀 필요 없어. 그냥 Python 리스트고, '트리'라는 건 그 인덱스를 해석하는 방식일 뿐이야.
이동은 전부 산술
이진 트리에서 봤던 그 공식이 여기서 제 몫을 해. 인덱스 i에 있는 원소를 기준으로 보면 이래.
- 왼쪽 자식은
2i + 1 - 오른쪽 자식은
2i + 2 - 부모는
(i − 1) // 2
루트 쪽으로 '트리를 올라가려면' i = (i-1)//2를 반복하면 되고, 내려가려면 두 배 하고 더하면 돼. 포인터를 따라갈 일도, 메모리를 새로 잡을 일도 없어. 트리를 돌아다니는 게 배열 인덱스에 대한 순수한 정수 산술인 거야. 힙 연산이 그렇게 빠른 이유가 정확히 이거고. 구조가 한 줄로 붙어 있으니 CPU 캐시가 계속 따뜻하게 유지되거든. 배열과 연결 리스트에서 배운 게 또 한 번 힘을 발휘하는 순간이야.
왜 complete를 지켜야 할까
완전성이라는 요구는 장식이 아니야. 배열에 빈틈이 없게 하고 높이를 정확히 ⌊log₂ n⌋으로 붙들어 두는 장치지. 덕분에 모든 힙 연산이 배열 끝, 그러니까 다음 리프 자리나 마지막 리프 자리에서 넣고 빼고, 그다음 원소를 위나 아래로 옮겨 순서를 회복하면 돼. 힙 중간에 구멍이 생기는 걸 허용하면 인덱스 산술이 깨지고 높이도 커질 수 있어. 완전성은 깔끔한 배열 배치와 O(log n) 높이를 동시에 보장해 주는 규율이야.
피파의 고백
2i+1이 매번 왼쪽 자식에 정확히 떨어지는 걸 보여줬어. 트리가 사라진 게 아니었어. 알고 보니 처음부터 옷만 갈아입은 배열이었던 거지. 트리라는 개념과 배열이라는 현실이 하나로 합쳐지던 그 순간이, 이 퀘스트 전체에서 내가 제일 좋아하는 '아, 겁먹었던 것보다 단순하네' 순간이야.