힙 정렬 (Heap Sort)
그리고 우선순위 큐 (Priority Queue)
소개
도널드 커누스는 힙 정렬에 대해 다음과 같이 말한다.
한편, [[/algorithm/quick-sort]]{빠른정렬}은 오직 평균 경우에서만 효율적이며, 해당 최악의 경우는 \(N^2\)의 규모이다. 힙 정렬은 최악의 경우가 평균 경우에 비해 아주 떨어지지는 않는다는 흥미로운 성질을 가지고 있다. 프로그램 H에서는 항상
\(A \ge 1.5 N\) , \(B \ge N \floor{ \lg N }\) , \(C \ge N \floor{ \lg N }\)
가 성립하며, 따라서 입력 자료의 분포와는 무관하게 \(18N \floor{ \lg N } + 38 N\) 단위 이상의 시간을 소비하는 경우는 없다. 지금까지 나온 정렬 방법들 중 정렬 시간이 \(N \log N\)의 규모임을 보장하는 것은 힙 정렬뿐이다. 5.2.4 절에서 설명하는 병합 정렬도 이러한 성질을 더 가지고 있지만, 메모리 공간을 더 많이 소비한다. 1
- A, B, C 세 단계에 대해서는 TAOCP 3권을 참고할 것.
로버트 세지윅은 힙 정렬에 대해 다음과 같이 언급한다.
힙-정렬은 정렬 알고리즘의 복잡도 연구에 있어서 대단히 중요한 역할을 한다. 왜냐하면 힙-정렬은 시간과 공간 양 측면 모두 최적 성능(상수 비율로)을 보이는 유일한 알고리즘이기 때문이다. 힙-정렬은 최악 조건에서 \(~ 2N \lg N\) 번의 비교 횟수와 상수 크기의 추가 공간 사용을 보증한다. 메모리 용량 사정이 빠듯한 환경에서는(예를 들어 임베디드 시스템이나 저가형 모바일 기기) 힙-정렬이 많이 사용된다. 단지 몇십 줄의 코드 추가만으로(심지어 기계어로도 마찬가지이다) 최적 성능을 얻을 수 있다. 하지만 전형적인 현대의 시스템에서는 드물게 사용된다. 왜냐하면 캐시 활용 효율을 떨어뜨리기 때문이다. 배열에서 인접한 항목끼리 비교되는 상황이 드물기 때문에 [[/algorithm/quick-sort]]{퀵-정렬}, [[/algorithm/merge-sort]]{병합 정렬}, 심지어 [[/algorithm/shell-sort]]{셸-정렬}과 비교해서도 캐시 미스(cache miss)의 발생 횟수가 훨씬 더 많다.
반면에 우선순위 큐의 구현에 힙을 이용하는 것은 현대의 응용 환경에서 점점 더 중요해지고 있다. 왜냐하면 아주 많은 수의 데이터 삽입 작업 또는 최대(또는 최소) 항목 삭제 작업이 서로 섞여 있는 경우에도 로그 시간 성능을 보증해 주기 때문이다. 2
로버트 세지윅은 힙 정렬이 전통적이면서 우아한 정렬 알고리즘(classic elegant sorting algorithm known as heapsort)이라 평하기도 했다.
Algorithms In A Nutshell에서는 힙 정렬을 스포츠 경기 토너먼트에 비유했다.
스포츠에서 토너먼트는 \(n\)개의 팀 가운데에서 '최고'의 팀을 뽑는 방식이지만, 최종 승자가 다른 \(n-1\)개의 팀과 모두 대결하라고 강요하지는 않는다. 미국에서 가장 인기 있는 농구 경기 중 하나인 NCAA 선수권 토너먼트는 본래 64개 대학팀이 선수권 우승을 향해 경쟁한다. 최종 우승팀은 결승전에 오르기까지 5개 팀과 경기를 하므로 모두 6번을 이겨야 한다. \(6 = \log (64)\)인 것은 우연이 아니다. 힙 정렬(Heap Sort)은 이러한 동작을 집합 원소의 정렬에 이용한다. 3
heap의 어원
모래나 곡식이 원뿔형으로 쌓여 있는 모양을 heap 이라 한다.
힙 정렬된 이진 트리
Definition. A binary tree is heap-ordered if the key in each node is larger than or equal to the keys in that node’s two children (if any).
정의. 이진 트리에서 각 노드의 두 자식 노드(만약 있다면)의 키 값이 부모 노드의 키 값보다 작으면 그 이진 트리는 힙-정렬되었다고 한다.2
즉, 힙-정렬 되었을 때 가장 큰 값은 루트 노드에 있다.
다음 이진 트리는 힙 정렬되었다고 할 수 있다.
아무 노드나 잡고 부모 노드로 계속 따라 올라가면 오름차순 시퀀스를 얻을 수 있다는 특징이 있다. 위의 정렬된 힙을 예로 들자면 다음과 같다.
- 15, 18, 21
- 19, 21
- 14, 19
이진 힙 (binary heap)
일반적으로 이진 힙을 그냥 힙이라고 부른다.
Definition. A binary heap is a collection of keys arranged in a complete heap-or- dered binary tree, represented in level order in an array (not using the first entry).
정의. 이진 힙은 힙-정렬된 완전 이진 트리의 노드들이 그 트리 레벨 순서대로 배열에 나열된 것이다(단 배열의 첫 번째 항목은 이용하지 않는다).2
예를 들어 위와 같이 힙-정렬된 완전 이진 트리가 있다면, 다음과 같은 배열을 만든다.
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
value | null | (21) | (18) | (19) | (15) | (2) | (16) | (14) |
힙을 이런 방식으로 배열에 저장하면 다음과 같이 부모 노드와 자식 노드를 찾을 수 있다.
- 인덱스 \(k\)에 있는 노드의 부모는 \(\floor{ k/2 }\)에 있다.
- 예: 배열 인덱스
6
에 있는(16)
의 부모 노드는 \(\floor{ 6 / 2 } = 3\) 이므로3
에 있는(19)
이다. - 예: 배열 인덱스
3
에 있는(19)
의 부모 노드는 \(\floor{ 3 / 2 } = \floor{ 1.5 } = 1\) 이므로1
. 즉 루트 노트이다.
- 예: 배열 인덱스
- 인덱스 \(k\)에 있는 노드의 두 자식 노드는 \(2k\)와 \(2k+1\)에 있다.
- 예: 배열 인덱스
3
에 있는(19)
의 자식 노드는6
에 있는(16)
과7
에 있는(14)
이다.
- 예: 배열 인덱스
힙 복구 (reheapifying)
힙 복구 작업으로 swim과 sink가 대표적이다. swim은 상향식으로, 노드를 타고 올라가면서 힙의 정렬 구조를 복구한다. 반대로 sink는 하향식이다. 힙을 내려가면서 복구한다.
로버트 세지윅은 힙 복구 알고리즘을 회사의 조직도에 비유해 재미있게 설명한다.
swim()
작업은 신입 사원이 들어 왔을 때 그의 능력에 맞게 그보다 능력이 나은 상관을 만날 때까지 반복해서 승진시키는 것이다(능력이 부족한 상관이 있다면 자리를 바꾸면서). 그리고sink()
작업은 사장이 사임해서 새로운 사장이 왔을 때 그 사장의 능력이 부족하다면 자기보다 능력이 낮은 사람들만 부하 직원으로 가지도록 반복해서 직급을 강등시키는 것이다.2
swim
다음과 같이 힙 정렬 상태를 불완전하게 하는 노드 23
이 있다고 하자.
swim 방식을 사용하면 23
이 다음과 같이 루트 노드까지 헤엄쳐 올라가면서(부모 노드와 자리를 바꾸면서) 힙 정렬된 상태로 복구한다.
/**
* k 노드의 위치를 swim 방식으로 복구한다.
*
* @param k 정렬 상태를 위반하는 노드의 배열 인덱스
*/
private void swim(int k) {
int rootNode = 1;
while ( k > rootNode && array[k/2] < array[k] ) {
// k 가 루트 노드가 아니고 부모 노드보다 값이 크다면, 부모 노드와 위치를 바꾼다.
int temp = array[k];
array[k] = array[k/2];
array[k/2] = temp;
k = k/2;
}
}
sink
다음과 같이 힙 정렬 상태를 불완전하게 하는 노드 17
이 있다고 하자.
sink 방식을 사용하면 17
이 가라앉으면서(자식 노드와 자리를 바꾸면서) 힙 정렬된 상태로 복구한다.
/**
* k 노드의 위치를 sink 방식으로 복구한다.
*
* @param k 정렬 상태를 위반하는 노드의 배열 인덱스
*/
private void sink(int k) {
int N = array.length;
while ( 2 * k <= N ) {
// 최하단 노드까지 도달하지 않았다면
int j = 2 * k;
if (j < N && array[j] < array[j+1]) {
// 두 자식 노드 중 더 큰 값을 가진 노드를 선택한다
j++;
}
if (array[k] >= array[j]) {
// 선택한 자식 노드가 sink 노드의 값보다 작으면 작업을 끝낸다
break;
}
// 그렇지 않다면 선택한 자식 노드와 sink 노드의 위치를 바꾼다
int temp = array[k];
array[k] = array[j];
array[j] = temp;
k = j;
}
}
삽입과 삭제
데이터 구조 | 삽입 | 최대 항목 삭제 |
---|---|---|
정렬된 배열 | \(O(N)\) | \(O(1)\) |
비정렬된 배열 | \(O(1)\) | \(O(N)\) |
힙 | \(O( \log N )\) | \(O( \log N )\) |
삽입과 삭제에 한하여, 힙을 사용하면 최악의 경우에도 \(O( \log N )\)만큼의 성능을 보인다.
새로운 노드 삽입
- 힙을 저장하고 있는 배열의 끝 부분에(마지막 노드 바로 다음 인덱스) 새로운 값을 추가하고, 힙 사이즈에
+1
한다. - 새로 추가된 노드를 swim 시킨다.
최악의 경우에도 \(O( \log N )\)만큼의 성능을 보인다.
15개의 노드를 보관할 수 있는 길이 16짜리 배열에 다음과 같이 힙 하나가 정렬된 상태로 들어 있다고 하자.
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
value | null | (22) | (16) | (19) | (14) | (8) | (15) | (1) | (5) | (9) | (7) |
이 힙에 이미 추가된 노드들의 바로 뒤인 11
번 인덱스에 새로운 노드 (20)
을 추가한다고 하자.
그래프로 표현하면 다음과 같을 것이다.
다음과 같이 (20)
을 추가하면 정렬이 깨진 상태가 된다.
따라서 (20)
은 swim을 시켜서 힙 정렬된 상태로 돌려놓아야 한다. 한 단계씩 swim 시켜보자.
(8)
과 (20)
을 바꿨다. 그러나 아직 (16)
이 위에 있다.
(16)
과 (20)
을 바꿨다. (20)
위에는 (22)
가 있으므로 swim이 끝난다.
루트 노드의 삭제
- 힙의 루트 노드를 제거한다.
- 힙의 마지막 노드를 루트 노드로 옮긴다.
- 힙 사이즈를
-1
한다. - 루트 노드를 sinc 시킨다.
최악의 경우에도 \(O( \log N )\)만큼의 성능을 보인다.
루트 노드를 왜 삭제하는가?
- 힙의 루트 노드는 힙에 들어있는 모든 값들 중 가장 큰 값을 갖고 있다.
- 힙에서 루트 노드를 계속해서 빼내면, 우선순위 큐로 사용할 수 있다.
다음과 같이 정렬된 힙에서 루트 노드 (22)
를 제거한다고 하자.
루트 노드를 제거한 다음, 배열의 마지막에 있는 노드 (8)
를 루트 위치로 옮긴다.
이제 (8)
에 sinc 연산을 적용한다. (20)
과 (19)
중 더 큰 노드와 (8)
의 자리를 바꾼다.
(16)
과 (8)
이 자리를 바꾸고 sink 연산은 종료된다.
배열 사이즈 자동 조정
소박하긴 하지만, 이 방법을 쓰면 힙이 사용하는 배열 사이즈가 부족할 일은 없다.
insert
가 발생할 때마다 배열 사이즈를 두 배로 늘려준다.deleteMax
가 발생할 때마다 배열 사이즈를 반으로 줄인다.
이 외에도 다양한 방법이 있다.
다중 힙 (Multiway Heap)
상황에 따라 힙 정렬된 3중 트리(heap-ordered ternary tree)를 고려할 수 있다.
- 인덱스
k
에 있는 노드의 자식 노드 배열 인덱스는 \(3k-1, 3k, 3k+1\). - 인덱스
k
에 있는 노드의 부모 노드 인덱스는 \(\floor{ (k+1) / 3 }\).
이와 비슷한 방법으로 3개 이상의 자식 노드를 갖는 다중 트리로 응용할 수 있다.
다중 힙의 퍼포먼스 트레이드 오프
다중 힙을 고려할 때에는 다음 두 가지의 트레이드 오프를 염두에 두어야 한다.
- 노드 하나가 갖는 자식 노드의 수 \(d\) 가 늘어나면 트리의 높이도 낮아진다.
- \(\log_d N\).
- 자식 노드의 수가 늘어나므로 자식들 중에서 가장 큰 노드를 찾는 속도는 느려진다.
힙 정렬
최고 | 평균 | 최저 |
---|---|---|
\(O(n \log n)\) | \(O(n \log n)\) | \(O(n \log n)\) |
힙 정렬은 두 단계로 구성된다.
- 힙 구성(heap construction)
- 정렬 대상 배열을 heap으로 해석해 배열 아이템을 재배치한다.
- 정렬 취합(sortdown)
- 최하단 노드부터 하나씩 root로 올려 올바른 위치로 sink 시킨다.
코드를 읽어보면 우아한 논리가 눈에 들어온다.
다음은 로버트 세지윅의 예제 코드를 일부 수정하고 주석을 붙인 것이다.
/** 주어진 배열을 정렬한다. */
public static void sort(Comparable[] array) {
final int n = array.length;
for (int k = n / 2; k >= 1; k--) {
// 힙을 구성한다(힙의 최하단 레벨은 sink 할 수 없으니 제외).
sink(array, k, n);
}
int k = n;
while (k > 1) {
// 정렬 취합. 최하단부터 노드를 하나씩 root로 올려놓고 sink 시킨다.
exchange(array, 1, k--);
sink(array, 1, k);
}
}
/**
* k 인덱스 노드를 인덱스 n 지점까지 sink 시킨다.
*
* @param array 힙 노드가 보관되어 있는 배열
* @param k sink 대상 노드 인덱스
* @param n sink 대상이 되는 부분 힙의 경계 인덱스
*/
private static void sink(Comparable[] array, int k, int n) {
while (2 * k <= n) {
int j = 2 * k;
if (j < n && less(array, j, j + 1)) {
j++;
}
if (!less(array, k, j)) {
break;
}
exchange(array, k, j);
k = j;
}
}
private static boolean less(Comparable[] array, int i, int j) {
return array[i - 1].compareTo(array[j - 1]) < 0;
}
private static void exchange(Comparable[] array, int i, int j) {
Comparable swap = array[i - 1];
array[i - 1] = array[j - 1];
array[j - 1] = swap;
}
이 코드(알고리즘 2.7)에 대해 로버트 세지윅은 다음과 같이 말한다.
알고리즘 2.7은 이러한 아이디어를 바탕으로 한 온전한 구현으로, 1964년 힙-정렬을 발명한 윌리엄스(J.W. Williams)와 힙-정렬을 개선한 플로이드(R.W. Floyd)의 전통적인 방식을 따르고 있다. 비록 이 코드의 루프가 각각 서로 다른 작업을 하는 듯이 보이지만(첫 번째는 힙을 구성하고 두 번째는 힙을 파괴4하면서 정렬을 취합한다) 두 루프 모두
sink()
메서드를 기반으로 하고 있다.
힙 구성, 정렬 취합과 관련된 다음 두 명제는 증명된 것이다. 증명은 어렵지 않으므로 생략한다.
명제 R.
sink()
작업에 기반한 힙 구성은 N개의 항목에 대해 수행할 때2N
보다 적은 수의 비교 연산과N
보다 적은 수의 교환 연산을 사용한다.명제 S. 힙-정렬은
N
개 항목을 정렬하는데 \(2N \lg N + 2N\)보다 적은 비교 연산(그리고 그 절반만큼의 교환 연산)을 수행한다. 2
Java의 PriorityQueue
Java는 1.5 버전부터 java.util
패키지에 PriorityQueue
클래스를 제공하고 있다. (PriorityQueue
주석을 읽어보면 Josh Bloch와 Doug Lea가 작성자이다.)
PriorityQueue
는 위에서 언급한 힙 정렬과 같이 가장 작은 값을 루트 노드로 위치시키는 방법으로 순서를 유지한다.
다음은 Java 11 버전 PriorityQueue
클래스의 queue
멤버에 대한 주석이다. 클래스 주석보다 짧으면서 본질을 잘 알려준다.
/**
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
*/
transient Object[] queue; // non-private to simplify nested class access
다음 번역은 내가 한 것이다.
균형 이진 트리로 표현된 우선순위 큐(Priority queue).
queue[n]
노드의 두 자식 노드는queue[2*n+1]
과queue[2*(n+1)]
입니다. 우선순위 큐는comparator
에 의해 정렬되며comparator
가null
인 경우에는 원소의 자체적인 순서(Comparable
구현한 것을 말함)에 따라 정렬됩니다. 힙에 포함된 모든 노드n
과 그 자식 노드d
에 대해n <= d
가 성립합니다.queue
가 비어 있지 않다면, 가장 작은 값은queue[0]
에 있습니다.
읽어보는 김에 클래스 주석도 함께 읽어보자.
/**
* An unbounded priority {@linkplain Queue queue} based on a priority heap.
* The elements of the priority queue are ordered according to their
* {@linkplain Comparable natural ordering}, or by a {@link Comparator}
* provided at queue construction time, depending on which constructor is
* used. A priority queue does not permit {@code null} elements.
* A priority queue relying on natural ordering also does not permit
* insertion of non-comparable objects (doing so may result in
* {@code ClassCastException}).
*
* <p>The <em>head</em> of this queue is the <em>least</em> element
* with respect to the specified ordering. If multiple elements are
* tied for least value, the head is one of those elements -- ties are
* broken arbitrarily. The queue retrieval operations {@code poll},
* {@code remove}, {@code peek}, and {@code element} access the
* element at the head of the queue.
*
* <p>A priority queue is unbounded, but has an internal
* <i>capacity</i> governing the size of an array used to store the
* elements on the queue. It is always at least as large as the queue
* size. As elements are added to a priority queue, its capacity
* grows automatically. The details of the growth policy are not
* specified.
*
* <p>This class and its iterator implement all of the
* <em>optional</em> methods of the {@link Collection} and {@link
* Iterator} interfaces. The Iterator provided in method {@link
* #iterator()} and the Spliterator provided in method {@link #spliterator()}
* are <em>not</em> guaranteed to traverse the elements of
* the priority queue in any particular order. If you need ordered
* traversal, consider using {@code Arrays.sort(pq.toArray())}.
*
* <p><strong>Note that this implementation is not synchronized.</strong>
* Multiple threads should not access a {@code PriorityQueue}
* instance concurrently if any of the threads modifies the queue.
* Instead, use the thread-safe {@link
* java.util.concurrent.PriorityBlockingQueue} class.
*
* <p>Implementation note: this implementation provides
* O(log(n)) time for the enqueuing and dequeuing methods
* ({@code offer}, {@code poll}, {@code remove()} and {@code add});
* linear time for the {@code remove(Object)} and {@code contains(Object)}
* methods; and constant time for the retrieval methods
* ({@code peek}, {@code element}, and {@code size}).
*
* <p>This class is a member of the
* <a href="{@docRoot}/java.base/java/util/package-summary.html#CollectionsFramework">
* Java Collections Framework</a>.
*
* @since 1.5
* @author Josh Bloch, Doug Lea
* @param <E> the type of elements held in this queue
*/
@SuppressWarnings("unchecked")
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
분량이 상당하니 핵심이 되는 몇몇 부분만 발췌해 요약해 보자.
- 우선순위 힙 방식을 구현한, 크기 제한이 없는 우선순위 큐.
- 원소들은
Comparable
순서나, 생성자에 제공된comparator
에 의해 정렬된다. null
원소를 허용하지 않는다.head
노드는 지정된 정렬방식으로 얻은 가장 작은 값을 갖습니다.- 크기 제한이 없다고 했지만 내부적으로 원소를 배열로 보관하고 있으므로, 용량(capacity)이 있다.
- 원소가 추가되면 용량이 자동으로 증가하게 구현되어 있다.
- 이 구현체는 스레드 안전하지 않다.
- 이 구현체의 enqueuing과 dequeuing 기능을 구현한 메소드들의 시간 복잡도는 \(O( \log n )\) 이다.
add, offer 메소드
새로운 값을 추가하는 offer
, add
메소드 코드를 읽어보자. 한국어 주석은 내가 추가한 것이다.
/**
* Inserts the specified element into this priority queue.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws ClassCastException if the specified element cannot be
* compared with elements currently in this priority queue
* according to the priority queue's ordering
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length) {
// 용량을 초과할 것 같으면 용량을 늘려준다
grow(i + 1);
}
// 새로운 노드를 바닥에 추가하고, swim 시켜서 올려보낸다
siftUp(i, e);
// 사이즈를 갱신하고 끝낸다
size = i + 1;
return true;
}
/**
* 생략: offer와 똑같다.
*/
public boolean add(E e) {
return offer(e);
}
shiftUp 메소드 (swim 역할)
이번엔 swim 역할을 하는 shiftUp
메소드를 읽어보자. 이번에도 한국어 주석은 내가 작성한 것이다.
PriorityQueue
는 값이 작을수록 상위 노드로 올라가게 되므로, 부모가 swim 연산 노드보다 작은 값을 가지면 swim이 종료된다.
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x, queue, comparator);
else
siftUpComparable(k, x, queue);
}
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
// 부모 노드
int parent = (k - 1) >>> 1;
Object e = es[parent];
// 부모 노드보다 값이 크다면 swim을 멈춘다.
if (key.compareTo((T) e) >= 0)
break;
// 그렇지 않다면 부모 노드와 자리를 바꾼다.
es[k] = e;
k = parent;
}
es[k] = key;
}
poll 메소드
poll
메소드는 다음 예제와 같이 최상단 노드, 즉 head
의 값을 dequeuing 한다.
PriorityQueue<Comparable> queue = new PriorityQueue<>();
queue.addAll(List.of(5, 6, 12, 1, 95, 7, 53));
for (int i = 0; i < 7; i++) {
System.out.print(queue.poll() + ", ");
}
출력 결과는 다음과 같다.
1, 5, 6, 7, 12, 53, 95,
poll
메소드의 코드는 다음과 같다.
public E poll() {
final Object[] es;
final E result;
if ((result = (E) ((es = queue)[0])) != null) {
modCount++;
final int n;
// 마지막 노드 x를 선택한다.
final E x = (E) es[(n = --size)];
// 마지막 노드가 있었던 곳에 null 을 입력한다.
es[n] = null;
if (n > 0) {
// 노드 x 를 최상단부터 sink 시킨다
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
return result;
}
shiftDown 메소드 (sink 역할)
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x, queue, size, comparator);
else
siftDownComparable(k, x, queue, size);
}
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
// 왼쪽과 오른쪽 두 자식 노드들 중 더 작은 노드를 선택한다.
int child = (k << 1) + 1; // assume left child is least
Object c = es[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
c = es[child = right];
// 자식 노드 선택이 끝났다면 자식과 값을 비교한다.
// 자식 노드보다 값이 작다면 sink를 멈춘다.
if (key.compareTo((T) c) <= 0)
break;
// 그렇지 않다면 자식 노드와 자리를 바꾼다.
es[k] = c;
k = child;
}
es[k] = key;
}
참고문헌
- [ROB] 알고리즘 [개정4판] / 로버트 세지윅, 케빈 웨인 저/권오인 역 / 길벗 / 초판발행 2018년 12월 26일
- [GEO] 사전처럼 바로 찾아 쓰는 알고리즘 / 조지 T. 하인만, 게리 폴리케, 스탠리 셀코 공저 / 전경원 역 / 한빛미디어 / 초판 2쇄 2011년 10월 20일 / 원제: Algorithms In A Nutshell
- [TAOCP] The Art of Computer Programming 3 정렬과 검색(개정2판) [양장] / 도널드 커누스 저 / 한빛미디어 / 초판 2쇄 2013년 02월 10일