👉🏻 힙 정렬 (Heap Sort) : 힙(Heap) 데이터 구조를 기반으로 최대·최소 힙 트리를 구성해 정렬하는 비교 기반 정렬 기술입니다.
- 힙(Heap)은 완전 이진 트리를 기반으로 하는 특별한 데이터 구조입니다. 힙 정렬(Heap Sort)는 그러한 힙(Heap) 데이터 구조를 이용해 정렬하는 알고리즘 기술입니다. 따라서 🚫 힙 정렬에 대해 공부하기 전에 힙 데이터 구조에 대해 선수학습을 한 후에 보는 것을 권합니다.❗️
O(n log n)
의 시간복잡도를 가지므로 대규모 데이터 정렬에 효율적입니다.최악의 경우에도 상한이 O(n log n)
의 시간으로 보장되어, 컴퓨터 공학 다방면에서 쓰이는 정렬입니다.
O(n log n)
이 됩니다.
[JavaScript로 힙 정렬하는 모습을 시각화 👉🏻 GitHub repository]
function heapify(arr, lastIdx, parentIdx) {
// 부모 idx를 가장 큰 값이라고 가정합니다.
let maxIdx = parentIdx;
// 현재 트리에서 자식 노드의 idx를 구합니다.
let left = 2 * parentIdx + 1;
let right = 2 * parentIdx + 2;
// 자식 노드의 인덱스가 끝 인덱스를 넘지 않고, 왼쪽 자식 노드의 값이 부모 노드의 값보다 크다면
// max에 왼쪽 노드 인덱스를 넣어준다.
if(left < lastIdx && arr[left] > arr[maxIdx]) maxIdx = left;
// 오른쪽 자식 노드에도 동일하게 조건을 검사한다.
if(right < lastIdx && arr[right] > arr[maxIdx]) maxIdx = right;
// maxIdx의 값이 바뀌었다면
if(maxIdx !== parentIdx) {
// 노드의 위치를 바꾸어 줍니다.
[arr[parentIdx], arr[maxIdx]] = [arr[maxIdx], arr[parentIdx]];
// 재귀호출을 이용해 바뀐 자식노드의 idx를 부모노드의 파라미터해, 부모노드로 삼은 서브트리를 다시 검사합니다.
heapify(arr, lastIdx, maxIdx);
}
}
function buildHeap(arr) {
let len = arr.length;
// 가장 마지막 노드의 부모 노드 서브트리부터 root 노드까지 검사합니다.
// ❗️ 자식 노드가 없는 부모노드는 검사할 게 없으므로 가장 마지막 노드부터 검사하는 것이 아닙니다!
for(let i = Math.floor(len / 2) -1; i >= 0; i--) {
heapify(arr, len, i);
}
}
우선 최대힙을 구성한 다음, root의 최댓값을 맨 마지막 노드로 보내고 그 노드를 제외한 나머지를 다시 최대힙을 정렬하는 과정을 반복하면서 맨 뒤부터 큰 수로 채워서 정렬합니다.
function heapify(arr, lastIdx, parentIdx) {
let minIdx = parentIdx;
let left = 2 * parentIdx + 1;
let right = 2 * parentIdx + 2;
if(left < lastIdx && arr[left] < arr[minIdx]) minIdx = left;
if(right < lastIdx && arr[right] < arr[minIdx]) minIdx = right;
if(minIdx !== parentIdx) {
[arr[parentIdx], arr[minIdx]] = [arr[minIdx], arr[parentIdx]];
heapify(arr, lastIdx, minIdx);
}
}
function buildHeap(arr) {
let len = arr.length;
for(let i = Math.floor(len / 2) -1; i >= 0; i--) {
heapify(arr, len, i);
}
// 최댓값을 마지막으로 보내고 다시 정렬하는 과정을 거칩니다.
for(let i = n - 1; i >= 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
heapify(arr, i, 0);
}
heapify를 진행하는 과정에서 재귀적으로 계속 호출을 하므로 배열이 크다면 stackoverflow의 위험이 있습니다. 이를 개선하기 위해 재귀 호출 대신 반복문으로 구현할 수 있습니다.
function heapify(arr, lastIdx, parentIdx) {
let maxIdx;
let left;
let right;
// 현재 부모 인덱스의 자식 노드 인덱스가 마지막 인덱스를 넘지 않을 때까지 반복한다.
// 마지막 부모 인덱스가 왼쪽 자식 노드만 가지고 있을 경우를 고려해,
// 왼쪽 자식 노드를 기준으로 한다.
while(parentIdx * 2 + 1 <= lastIdx) {
left = 2 * parentIdx + 1;
right = 2 * parentIdx + 2;
maxIdx = parentIdx;
// 왼쪽 자식 노드 비교에서 left <= lastIdx 부분은 while문 조건에서 하고 있으므로 제외해준다.
if(arr[left] > arr[maxIdx]) maxIdx = left;
if(right <= lastIdx && arr[right] > arr[maxIdx]) maxIdx = right;
if(maxIdx !== parentIdx) {
[arr[parentIdx], arr[maxIdx]] = [arr[maxIdx], arr[parentIdx]];
// 교환 후에 교환된 자식노드를 부모노드가 되도록 idx를 재할당 해준다.
parentIdx = maxIdx;
}
else return;
}