힙정렬
11 Dec 2019 | Algorithm힙정렬
힙정렬이란?
- 힙이라는 자료구조를 이용한다.
힙이란 ?
-
힙(heap)은 최댓값 및 최솟값을 찾아내는 연산을 빠르게 하기 위해 고안된 완전이진트리[1]를 기본으로 한 자료구조(tree-based structure)로서 다음과 같은 힙 속성(property)을 만족한다.
- A가 B의 부모노드(parent node) 이면, A의 키(key)값과 B의 키값 사이에는 대소관계가 성립한다.
- Key[A] > Key[B] : 최대 힙
- Key[A] < Key[B] : 최소 힙
힙은 다음과 같이 1차원 배열로도 표현이 가능하다.
어떤 노드의 인덱스를 index,
왼쪽 자식노드의 인덱스를 left_index,
오른쪽 자식노드의 인덱스를 right_index로 선언하면 다음과 같은 관계를 지닌다.
// 인덱스가 0부터 시작하는 경우
Parent(i) = i/2;
left_index(i) = 2i+1;
right_index(i) = 2i+2;
힙 정렬 알고리즘은 최대힙을 사용하며 3가지 연산이 필요하다.
- Max-Heapify
- Build Max-Heap
- HeapSort
Max-Heapify
- 최대 힙 특성을 유지하기 위한 연산
- Max-Heapify를 호출할때 left_index(i), right_index(i)를 루트로 하는 이진트리는 최대 힙이라 가정한다.
- 시간복잡도는 높이에 비례하므로 O(logn)이다 시간복잡도 증명
// pseudocode
MaxHeapify(A,i)
l = left_index(i);
r = right_index(i);
if(l <= A.heap_size && A[l] > A[i])
largest = l;
else largest = i;
if(r <= A.heap_size && A[r] > A[largest])
largest = r;
if(largest != i)
exchange A[i] with A[largest]
MaxHeapify(A,largest);
Build Max-Heap
- 임의의 배열을 최대 힙으로 구성하는 연산
// pseudocode
BuildMaxHeap(A)
A.heap.size = A.length
for i=A.length/2 downto 1
MaxHeapify(A,i)
-
i=length/2부터 시작하는 이유는 length/2+1부터는 리프 노드이므로 MaxHeapify가 필요 없기 때문이다
-
시간복잡도는 O(logn) * n/2 이므로 O(nlogn)이나, 이것은 루트 노드를 기준으로 러프하게 계산한 것이므로
좀 더 정확하게 계산하면 O(n)이다.
HeapSort
-
힙 정렬은 다음과 같은 방법으로 수행된다
- 주어진 데이터를 힙으로 만든다.
- 힙에서 최대값(루트 노드)을 가장 마지막 값과 바꾼다.
- 힙의 크기가 1 줄어든 것으로 간주한다. 즉, 마지막 값은 힙의 일부가 아닌것으로 간주한다.
- 루트 노드에 대해서 HEAPIFY(1)한다.
- 2~4번을 반복한다.
- 주어진 데이터를 힙으로 만든다.
-
시간복잡도는 O(n) + O(nlogn) = O(nlogn)
HeapSort(A)
BuildMaxHeap(A) // O(n)
for i= A.length downto 2 // O(nlogn)
exchange A[1] with A[i]
A.heap.size = A.heap.size - 1
MaxHeapify(A,1)
Java로 힙정렬 구현
- 테스트 코드
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
class HeapSortTest {
@Test
public void test() {
int[] input = new int[]{5, 13, 2, 25, 7, 17, 20, 8, 4};
HeapSort.Heap heap = new HeapSort.Heap(input);
int[] answer = Arrays.copyOf(input, input.length);
Arrays.sort(answer);
HeapSort.heapSort(heap);
assertArrayEquals(heap.getHeap(), answer);
}
}
- 알고리즘
public class HeapSort {
// 최대힙의 0번째 값(최대값)을 가장 뒤로 보내 정렬한다.
public static void heapSort(Heap heap){
buildMaxHeap(heap);
int tmp = Integer.MIN_VALUE;
for(int i = heap.length()-1; i>=0; i--){
swap(heap,i,0);
heap.decrementSize();
maxHeapify(heap,0);
}
}
private static void buildMaxHeap(Heap heap){
for(int i = heap.length()/2-1; i>=0; i--)
maxHeapify(heap,i);
}
private static void maxHeapify(Heap heap, int idx){
int leftIndex = getLeftIndex(idx);
int rightIndex = getRightIndex(idx);
int size = heap.size();
int largest = -1;
// 왼쪽 노드와 비교
if(leftIndex < size && heap.get(leftIndex) > heap.get(idx))
largest = leftIndex;
else
largest = idx;
// 오른쪽 노드와 비교
if(rightIndex < size && heap.get(rightIndex) > heap.get(largest))
largest = rightIndex;
// 부모 노드보다 자식노드가 큰 경우 교환
if(largest != idx){
swap(heap, idx, largest);
//재귀호출
maxHeapify(heap,largest);
}
}
private static void swap(Heap heap, int to, int from) {
int tmp = heap.get(to);
heap.set(to, heap.get(from));
heap.set(from, tmp);
}
private static int getLeftIndex(int idx){
return 2*idx+1;
}
private static int getRightIndex(int idx){
return 2*idx+2;
}
public static class Heap {
int[] heap;
int size;
public Heap(int[] src){
heap = src;
size = heap.length;
}
public int size(){
return size;
}
public int length(){
return heap.length;
}
public void decrementSize(){
size--;
}
public int get(int idx){
return heap[idx];
}
public int[] getHeap(){
return heap;
}
public void set(int idx, int value){
heap[idx] = value;
}
}
}
[1]
- 완전이진트리(complete binary tree)
- 마지막 레벨을 제외하고 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 모든 노드는 가능한 한 가장 왼쪽에 있다. 마지막 레벨 h 에서 1부터 2h-1 개의 노드를 가질 수 있다.
- 포화이진트리(perfect binary tree)
- 모든 내부 노드가 두 개의 자식 노드를 가지며 모든 잎 노드가 동일한 깊이 또는 레벨을 갖는다
ref. 이진트리 이미지