Files
hello-algo/ko/docs/chapter_heap/build_heap.md
T
Yudong Jin 2f561d3d51 feat(ko): add the Korean online edition (#2006)
* feat(ko): add the complete Korean online edition

* Use UTF-8 for Python CI output on Windows

* Remove print-book promotion from the Korean site

* fix(ko): preserve emphasis before Korean particles
2026-10-29 03:46:43 +08:00

4.5 KiB

힙 구성 연산

어떤 상황에서는 리스트의 모든 원소를 사용해 힙을 구성하고 싶을 수 있다. 이 과정을 '힙 구성 연산'이라고 한다.

힙 삽입 연산을 이용한 구현

먼저 빈 힙을 만든 다음, 리스트를 순회하면서 각 원소에 대해 차례로 '힙 삽입 연산'을 수행한다. 즉, 원소를 힙의 끝에 추가한 뒤 해당 원소부터 '아래에서 위로' 힙을 재정렬한다.

원소 하나를 힙에 삽입할 때마다 힙의 길이가 1씩 늘어난다. 이진 트리에 위에서 아래 순서로 노드를 추가하므로 힙은 '위에서 아래로' 구성된다.

원소의 개수를 $n$이라고 하자. 각 원소의 힙 삽입 연산에는 O(\log{n}) 시간이 걸리므로, 이 힙 구성 방법의 시간 복잡도는 $O(n \log n)$이다.

순회하며 힙을 재정렬하는 구현

실제로는 다음 두 단계로 더 효율적인 힙 구성 방법을 구현할 수 있다.

  1. 리스트의 모든 원소를 그대로 힙에 추가한다. 이 시점에서는 아직 힙의 성질을 만족하지 않는다.
  2. 힙을 역순으로 순회하면서(레벨 순서 순회의 역순), 각 내부 노드에 대해 차례로 '위에서 아래로 힙 재정렬'을 수행한다.

노드 하나를 재정렬할 때마다 그 노드를 루트로 하는 서브트리가 올바른 부분 힙이 된다. 역순으로 순회하므로 힙은 '아래에서 위로' 구성된다.

역순으로 순회하는 이유는 현재 노드 아래의 서브트리들이 이미 올바른 부분 힙임을 보장하기 위해서다. 그래야 현재 노드를 재정렬하는 작업이 유효해진다.

리프 노드는 자식 노드가 없으므로 그 자체로 올바른 부분 힙이며, 재정렬할 필요가 없다. 아래 코드와 같이 마지막 내부 노드는 마지막 노드의 부모 노드다. 여기서부터 역순으로 순회하며 힙 재정렬을 수행한다.

[file]{my_heap}-[class]{max_heap}-[func]{__init__}

복잡도 분석

이제 두 번째 힙 구성 방법의 시간 복잡도를 계산해 보자.

  • 완전 이진 트리의 노드 수가 $n$이라면, 리프 노드의 수는 $(n + 1) / 2$이다. 여기서 $/$는 내림 정수 나눗셈이다. 따라서 재정렬해야 하는 노드의 수는 $n / 2$이다.
  • 위에서 아래로 힙을 재정렬할 때 각 노드는 최대 리프 노드까지 내려갈 수 있으므로, 최대 반복 횟수는 이진 트리의 높이인 $\log n$이다.

위의 두 값을 곱하면 힙 구성 과정의 시간 복잡도가 $O(n \log n)$이 된다. 하지만 이 추정은 정확하지 않다. 이진 트리에서는 아래쪽 노드의 수가 위쪽 노드보다 훨씬 많다는 성질을 고려하지 않았기 때문이다.

이제 더 정확하게 계산해 보자. 계산을 간단히 하기 위해 노드 수가 $n$이고 높이가 $h$인 '포화 이진 트리'가 주어졌다고 가정한다. 이 가정은 계산 결과의 정확성에 영향을 주지 않는다.

포화 이진 트리의 레벨별 노드 수

위 그림에서 노드를 '위에서 아래로 재정렬'하는 최대 반복 횟수는 그 노드에서 리프 노드까지의 거리와 같으며, 이 거리는 바로 '노드의 높이'다. 따라서 각 레벨의 '노드 수 \times 노드 높이'를 더하면 모든 노드에 대한 힙 재정렬의 총반복 횟수를 구할 수 있다.


T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1

위 식을 정리하려면 중등 수학의 수열 지식이 필요하다. 먼저 $T(h)$에 $2$를 곱하면 다음과 같다.


\begin{aligned}
T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline
\end{aligned}

두 식의 항을 한 칸씩 어긋나게 놓고 아래 식 $2 T(h)$에서 위 식 $T(h)$를 빼면 다음을 얻는다.


2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h

위 식에서 $T(h)$를 이루는 항들이 등비수열을 이루므로 합 공식을 바로 적용하여 시간 복잡도를 구할 수 있다.


\begin{aligned}
T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
& = 2^{h+1} - h - 2 \newline
& = O(2^h)
\end{aligned}

또한 높이가 $h$인 포화 이진 트리의 노드 수는 $n = 2^{h+1} - 1$이므로, 복잡도가 $O(2^h) = O(n)$임을 쉽게 알 수 있다. 위 계산은 리스트를 입력받아 힙을 구성하는 시간 복잡도가 $O(n)$으로 매우 효율적임을 보여 준다.