/* n: 배열의 길이 */
public class HeapSort {
static void swap(int[] arr, int idx1, int idx2) {
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
/**
* 배열을 힙으로 변환
* */
static void downHeap(int[] arr, int n, int selectedIdx) {
int parentNode = selectedIdx;
int leftChildNode = parentNode*2 + 1;
int rightChildNode = parentNode*2 + 2;
// 1-1. 왼쪽 자식노드
if(leftChildNode < n && arr[parentNode] < arr[leftChildNode]) {
parentNode = leftChildNode;
}
// 1-2. 오른쪽 자식노드
if(rightChildNode < n && arr[parentNode] < arr[rightChildNode]) {
parentNode = rightChildNode;
}
// 1-3. 부모노드 < 자식노드
if(selectedIdx != parentNode) {
swap(arr, parentNode, selectedIdx); // 자식노드와 부모노드의 값 교환
downHeap(arr, n, parentNode);
}
}
/**
* 힙 정렬
* */
static void heapSort(int[] arr, int n) {
// 1. 배열을 최대 힙으로 구성
for(int i = n/2 - 1; i >= 0; i--) {
downHeap(arr, n, i);
}
// 2. 루트값 추출 및 최대힙 재구성
for(int i = n-1; i > 0; i--) {
swap(arr, 0, i); // 루트 값과 아직 정렬되지 않은 부분의 마지막 요소 교환
downHeap(arr, i, 0); // 추출한 루트값을 제외하고 힙 재구성
}
}
1. 일반 배열을 최대 힙으로 구성하기 위한 로직으로, 자식노드로부터 부모노드를 비교한다. 반복문에서 인덱스의 범위를 n/2-1
부터 0까지로 지정한 이유는, 부모노드의 인덱스를 기준으로 왼쪽 자식노드(i*2 + 1
)와 오른쪽 자식노드(i*2 + 2
)가 존재하고(n/2
를 하는 이유), 루트 노드의 인덱스는 0부터 시작하기 때문이다(n/2
에 -1
을 하는 이유).
1-1. 왼쪽 자식노드의 인덱스가 배열의 범위를 초과하지 않고(leftChildNode < n
), 부모노드의 값이 왼쪽 자식노드의 값보다 작으면(arr[parentNode] < arr[leftChildNode]
), 부모노드의 인덱스에 왼쪽 자식노드의 인덱스를 저장하여 위치를 조정해준다(parentNode = leftChildNode
). 이는 최대 힙에서 부모노드의 값은 자식노드의 값보다 작을 수 없기 때문이다.
1-2. 오른쪽 자식노드의 인덱스가 배열의 범위를 초과하지 않고(rightChildNode < n
), 부모노드의 값이 오른쪽 자식노드의 값보다 작으면(arr[parentNode] < arr[rightChildNode]
), 부모노드의 인덱스에 오른쪽 자식노드의 인덱스를 저장하여 위치를 저장해준다(parentNode = rightChildNode
). 이는 자식노드의 값은 부모노드의 값은 자식노드의 값보다 작을 수 없기 때문이다.
1-3. 만약 부모노드의 인덱스에 변화가 있는 경우, 부모노드와 자식노드 간에 위치 조정이 필요하므로 자식노드와 부모노드의 값을 교환한다(swap()
). 그리고 조정이 완료된 부모노드부터 다시 downHeap()
을 수행한다.
2. 최대 힙이 구성된 상태이므로, 루트 값과 아직 정렬되지 않은 부분의 마지막 요소의 값을 교환하여 루트 값을 추출한 후(swap()
), 추출한 루트값을 제외하고 힙을 재구성한다(downHeap()
). 반복문에서 인덱스를 n-1
로 초기화 한 이유는, 힙 정렬은 최하위 레벨부터 위로 진행하는 down-top
방식이기 때문이다. 또한, 힙 정렬의 마지막 단계에서 배열의 첫 번째 요소(루트 노드)만 남았을 경우, 그 요소가 이미 최대값이기 때문에 더 이상의 조정이 필요 없기 때문에 루트 노드를 제외하고 반복문을 수행하게 된다(i>0
).
public class HeapSort {
// O(1)
static void swap(int[] arr, int idx1, int idx2) {
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
// O(log n)
static void downHeap(int[] arr, int n, int selectedIdx) {
int parentNode = selectedIdx;
int leftChildNode = parentNode*2 + 1;
int rightChildNode = parentNode*2 + 2;
// O(1)
if(leftChildNode < n && arr[parentNode] < arr[leftChildNode]) {
parentNode = leftChildNode;
}
// O(1)
if(rightChildNode < n && arr[parentNode] < arr[rightChildNode]) {
parentNode = rightChildNode;
}
// O(log n)
if(selectedIdx != parentNode) {
swap(arr, parentNode, selectedIdx);
downHeap(arr, n, parentNode);
}
}
static void heapSort(int[] arr, int n) {
// O(n)
for(int i = n/2 - 1; i >= 0; i--) {
downHeap(arr, n, i);
}
// O(n log n)
for(int i = n-1; i > 0; i--) {
swap(arr, 0, i); // O(1)
downHeap(arr, i, 0); // O(log n)
}
}
}
O(n)
O(nlogn)
swap()
: O(1)downHeap()
: 최악의 경우 힙의 높이(logn) 만큼 걸림 → O(logn)O(logn) + O(log(n−1)) + O(log(n−2)) + ... + O(log2)
∴ O(nlogn)
T(n) = O(n) + O(nlogn)
∴ T(n) = O(nlogn)
이 글에서는 최대 힙만 이용했지만, 최소 힙으로도 구성이 가능하다. 최소 힙(최소값)/최대 힙(최대값)의 루트값이므로 한 번의 힙 구성을 통해 해당 값을 구하는 것이 가능하다.
k
만큼 떨어진 요소들을 정렬할 때삽입정렬보다 개선된 결과를 획득할 수 있다.
📖 참고
- 힙 소트(Heap Sort)
- 이관용. "알고리즘 11강. 정렬 알고리즘 (2)" 방송통신대학교, 2022년.