- 힙이란 무엇인가 ?
- 힙을 이용한 우선순위 큐 구현
- 힙 구현과 성능
- 힙 정렬
- 제자리 힙 정렬
- 상향식 힙 생성
- 힙(Heap) : 내부노드에 키를 저장하며 다음 두 가지 속성을 만족하는 이진트리
- 힙 순서(Heap-order) : 루트를 제외한 모든 내부노드 v에 대해
Key(v) >= Key(parent(v)) => (최소힙 기준)
Key(v) <= Key(parent(v)) => (최대힙 기준)
- 완전이진트리(Complete binary tree) : 힙의 높이가 h라고 하면
=> i = 0, ... , h - 1에 대해 깊이 i인 노드가 2^i개 존재
=> 깊이 h - 1에서, 내부노드들은 외부노드들의 왼쪽에 존재
- 힙의 마지막(Last Node) : 깊이 h - 1의 가장 오른쪽 내부노드

- 정리 : n개의 키를 저장한 힙의 높이는 O(log n)이다.
- 증명 : ( 완전이진트리의 성질을 이용 )
- n개의 키를 저장한 힙의 높이를 h라 하자.
- 깊이 i = 0, ... , h - 2 에 2^i개의 키, 그리고 깊이 h - 1에 적어도 한 개의 키가 존재하므로, n >= 1 + 2 + 4 + ... + 2^(h-2) + 1
- 따라서, n >= 2^(h-1) (등비수열의 합) ,즉 h <= log(n) + 1

- 힙을 사용하여 우선순위 큐(Priority queue) 구현 가능
- 전제 : 적정이진트리로 구현
- 마지막 노드의 위치를 관리
- 우선순위 큐 ADT의 메쏘드 InsertItem 은 힙에 키 k를 삽입하는 것에 해당
- 삽입 알고리즘의 세 단계
- 삽입 노드 z, 즉 새로운 마지막 노드를 찾는다.
- k를 z에 저장한 후, expandExternal(w) 작업을 사용하여 z을 내부노드로 확장
- 힙 순서 속성을 복구

- Upheap ( Continue. 힙 삽입 )
- 새로운 키 k가 삽입된 후, 힙 순서 속성이 위배될 수 있다.
- upheap은 삽입노드로부터 상향경로를 따라가며 키 k를 교환함으로써 힙 순서 속성을 복구
- upheap은 키 k가 루트에 또는 부모의 키가 k보다 작거나 같은 노드에 도달하면 정지 ( 최소힙 기준 ! , 최대힙은 반대 )
- 힙의 높이는 O(log(n))이므로 upheap은 O(log(n))시간에 수행
(앞서 배운, 선택정렬, 삽입정렬로 우선순위 큐와 비교했을 때 상당히 빠름)

| [ PseudoCode ] |
| ----------------------------- |
| |
| Alg InsertItem(k) |
| input Key k, node last |
| output none |
| |
| advanceLast( ) |
| z <- last |
| Set node z to k |
| expandExternal(z) |
| upHeap(z) |
| return |
| |
| ------------------------------ |
| |
| Alg upHeap(v) |
| input node v |
| output none |
| |
| if(isRoot(v)) |
| return |
| if(Key(v) >= Key(parent(v)) |
| return |
| swapElements(v,parent(v)) |
| upHeap(parent(v)) |
| |
| ------------------------------- |
- InsertItem 과 upHeap의 작업 대상 힙은 일반(generic)힙 이다.
일반 힙이 무엇인지 모르겠다면, "우선순위 큐"
- 힙 삽입 알고리즘 ( Continue. 힙 삽입 )
| [ PseudoCode ] |
| -------------------------------------------- |
| |
| Alg expandExternal(z) { 연결리스트 경우만 ! } |
| input external node z |
| output none |
| |
| l <- getnode() |
| r <- getnode() |
| l.right <- NULL |
| l.left <- NULL |
| l.parent <- z |
| r.right <- NULL |
| r.left <- NULL |
| r.parent <- z |
| z.left <- l |
| z.right <- r |
| return |
| |
| -------------------------------------------- |

: 내부노드화 시킨다는 소리
- 우선순위 큐 ADT의 메쏘드 removeMin은 힙으로부터 루트 키를 삭제하는 것에 해당 ( 최소 힙 기준 ! )
- 삭제 알고리즘의 세 단계
- 루트 키를 마지막 노드 w의 키로 대체
- reduceExternal(z) 작업을 사용하여 w와 그의 자식들을 외부노드로 축서
- 힙 순서 속성을 복구

- 루트 키를 마지막 노드의 키로 대체한 후, 힙 순서 속성이 위배될 수 있다.
- downheap은 루트로부터 하향 경로를 따라가며 키 k를 교환함으로써 힙 순서 속성을 복구
- downheap은 키 k가 리프노드, 또는 자식의 키가 k보다 크거나 같은 노드에 도달하면 정지 ( 최소 힙 기준 !, 최대 힙은 반대 )
- 힙의 높이는 O(log(n)) 이므로 downheap은 O(log(n)) 시간에 수행

| [ PseudoCode ] { 연결리스트 기준 } |
| ---------------------------------------------------------- |
| Alg removeMin() { 최소 힙 기준 } |
| input node last |
| output key |
| |
| k <- key(root()) |
| w <- last |
| Set root to key(w) |
| retreatLast() |
| z <- rightChild(w) |
| reduceExternal(z) |
| downHeap(root()) |
| return k |
| |
| ----------------------------------------------------------- |
| |
| Alg downHeap(v) |
| input node v whose left and right substrees are heaps |
| out a heap with root v |
| |
| if((isExternal(leftChild(v)) & isExternal(rightChild(v)) |
| return |
| smaller <- leftChild(v) |
| if(isInternal(rightChild(v)) |
| if(key(rightChild(v) < key(smaller)) |
| smaller <- rightChild(v) |
| if(key(v) <= key(smaller)) |
| return |
| swqpElements(v,smaller) |
| downHeap(smaller) |
| |
| ----------------------------------------------------------- |
| [ PseudoCode ] { 연결리스트 } |
| -------------------------------------------------------------------- |
| |
| Alg reduceExternal(z) |
| input external node z |
| output the node replacing the parent node of the removed node z |
| |
| w <- z.parent |
| zs <- sibling(z) |
| if(isRoot(w)) |
| root <- zs |
| zs.parent <- NULL |
| else |
| g <- w.parent |
| zs.parent <- g |
| if(w = g.left) |
| g.left <- zs |
| else |
| g.right <- zs |
| putnode(z) |
| putnode(w) |
| return zs |
| |
| ----------------------------------------------------------------------- |

- O(log(n))개의 노드를 순회함으로써 삽입 노드를 찾을 수 있다. (advanceLast)
- 현재 노드가 오른쪽 자식인 동안, 부모 노드로 이동
- 현재 노드가 왼쪽 자식이면, 형제 노드로 이동
- 현재 노드가 내부노드인 동안, 왼쪽 자식으로 이동
- 삭제 후 마지막 노드를 갱신하는 작업은 위한 반대 방향으로수행 (retreatLast)

- n개의 키를 가진 힙을 크기 n의 배열을 사용하여 표현 가능
- 첨자 i에 존재하는 노드에 대해
- 왼쪽 자식은 첨자 2*i 에 존재
- 오른쪽 자식은 첨자 2*i+1 에 존재
- 부모는 첨자 i/2에 존재
- 노드 사이의 링크는 명시적으로 저장할 필요가 없다.
( 다만, 머리 속으로 링크가 있는 것처럼 상상하자.)
- 외부노드들은 표현할 필요가 없다.
- 첨자 0 셀은 사용하지 않는다. ( 빈 노드 )
- 마지막 노드의 첨자 : 항상 n
- InsertItem : 작업은 첨자 n + 1위치에 삽입하는 것에 해당
- removeMin : 작업은 첨자 n 위치에서 삭제하는 것에 해당


- 힙 정렬 ( Heap Sort ) ( 최소힙 기준 )
- 힙을 사용하여 구현된 n-항목 우선순위 큐를 고려하면,
- 공간 사용량은 O(n)
- InsertItem 메쏘드와 removeMin 메쏘드는 O(log(n))시간에 수행
- size, isEmpty,minKey,minElement 메쏘드는 O(1)시간에 수행
( 힙의 루트노드정보만 조회하면 되니깐 )
- 힙에 기초한 우선순위 큐를 사용함으로써, n개의 원소로 이루어진 리스트를 O(log(n))시간에 정렬할 수 있다.
- 선택정렬이나 삽입정렬과 같은 2차 정렬 알고리즘보다 훨씬 빠르다.
- 이 알고리즘을 힙 정렬(Heap Sort) 라고 한다.
| [ PseudoCode ] |
| --------------------------------- |
| |
| Alg heapSort(L) |
| input list L |
| output sorted list L |
| |
| H <- empty heap |
| while(!L.isEmpty()) { 1 단계 } |
| k <- L.removeFirst() |
| H.insertItem(k) |
| while(!H.isEmpty()) { 2 단계 } |
| k <= H.removeMin() |
| L.addLast(k) |
| return |
| |
| --------------------------------- |
- Heap Sort 의 성능 향상을 위한 두 가지 개선점
- 제자리 힙 정렬은 heap sort 의 공간 사용을 줄인다.
- 상향식 힙 생성은 heap sort의 속도를 높힌다.
- 이 방식은 정렬되어야 할 리스트가 "배열" 로 주어진 경우에만 적용
- 힙을 저장하는데 리스트 L의 일부를 사용함으로써 외부 힙 사용을 피한다.
- 지금까지 사용했던 최소힙(min-heap) 대신, 최대 원소가 맨위에 오게 되는 최대힙(max-heap)을 사용
| [ PseudoCode ] |
| --------------------------------------------------------------- |
| |
| Alg InPlaceHeapSort(A) |
| input array A of n Keys |
| output sorted array A |
| |
| buildHeap(A) |
| for i <- n downto 2 |
| A[1] <-> A[i] |
| downHeap(1, i-1) |
| return |
| |
| ---------------------------------------------------------------- |
| |
| Alg buildHeap(A) |
| input array A of n Keys |
| output heap A of size n |
| |
| for i <- 1 to n |
| insertItem(A[i]) |
| return |
| |
| ---------------------------------------------------------------- |
| |
| Alg downHeap(i, last) |
| input index i of array A representing a maxheap of size last |
| output none |
| |
| left <- 2i |
| right <- 2i + 1 |
| if(left > last) |
| return |
| greater <- left |
| if(right <= last) |
| if(key(A[right] >= key(A[greater])) |
| greater <- right |
| if(key(A[i] >= key(A[greater])) |
| return |
| A[i] <-> A[greater] |
| downHeap(greater,last) |
| |
| --------------------------------------------------------------- |
- 알고리즘 수행의 어떤 시점에서든
- L의 첨자 1부터 i까지의 왼쪽 부분을 힙의 원소들을 저장하는데 사용
- 그리고 첨자 i+1부터 n까지의 오른쪽 부분을 리스트의 원소들을 저장하는데 사용
- 그러므로, L의 (첨자 1, ... , i에 있는) 첫 i개의 원소들은 힙의 배열 표현
=> 즉, 첨자 k의 원소는 첨자 2k 및 2k+1의 자식들보다 크거나 같다.

- 1기
- 비어 있는 힙으로부터 출발하여 힙과 리스트의 경계를 왼쪽에서 오른쪽으로 한 번에 한칸씩 이동
- 단계 i(i =1, ... , n)에서, 첨자 i에 있는 원소를 힙에 추가함으로써 힙을 확장
- 2기
- 비어 있는 리스트로부터 출발하여 힙과 리스트의 경계를 오른쪽에서 왼쪽으로 한 번에 한 칸씩 이동
- 단계 i(i = n, ... , 2)에서, 힙의 최대 원소를 삭제하여 리스트의 첨자 i에 저장함으로써 리스트를 확장
- 1기의 2기의 각 단계에서, 배열 가운데 힙에 쓰인 부분을 파란색 원소로 표시
- 아래 점선 내에 보인 이진트리 관점의 힙은 가상적일 뿐, 제자리 알고리즘에 의해 실제 생성되지는 않음에 유의 ( 배열로 구현했으니 )
- 1기 작업 시작




- heap-sort의 1기에서, n회의 연속적인 InsertItem 작업을 사용하여 O(nlog(n)) 시간에 힙을 생성했다.
- 대안으로, 만약 힙에 저장되어야 할 모든 키들이 미리 주어진다면, O(n)시간에 수행하는 상향식 힙 생성 방식있다. ( 뒤에 기술함. )
| [ PseudoCode ] |
| ---------------------------------------------------------------------------------- |
| |
| Alg buildHeap(L) // ( 재귀적, 배열인경우 ) |
| input list L storing n Keys |
| output heap T storing the Keys in L |
| |
| if(L.isEmpty()) |
| return empty heap |
| Remove the first key km from L |
| Split L into two sublists, L1, and L2, each of size (n-2)/2 approximately |
| |
| T1 <- buildHeap(L1) |
| T2 <- buildHeap(L2) |
| |
| Create a binary tree T with root r storing k, left subtree T1, and right subtree T2 |
| |
| downHeap(r) |
| return T |
| |
| ------------------------------------------------------------------------------------ |
- log n 단계만을 사용하여 주어진 n개의 키를 저장하는 힙 생성 가능
- 단계 i에서, 각각 2^i - 1 개의 키를 가진 두 개의 힙을 2^(i+1) -1개의 키를 가진 힙으로 합병

- 두 개의 힙과 키 k가 주어졌을 때
- k를 저장한 노드를 루트로, 두 개의 힙을 부트리로 하는 새 힙을 생성
- 힙 순서 속성을 복구하기 위해 downheap을 수행

- 상향식 힙 생성버전은 각 재귀호출이 힙인 부트를 반환하는 방식이기 때문에 "상향식" 이라 불린다.
- 다시 말해, T 의 힙합(heapification)는 외부노드에서 시작하여, 각 재귀호출이 반환함에 따라 트리 위쪽으로 진행
- 이 때문에 종종 힙화한다(heapify)고 말하기도 한다.





- 상향식 힙생성의 비재귀 버전
- 정렬되어야 할 리스트가 "배열"로 주어진 경우에만 적용
- 비 재귀적 힙 생성 절차가 "내부노드를 왼쪽 자식으로 가지는, 가장 깊은 내부노드 가운데 가장 오른쪽 노드"에서 시작하여 루트로 향하는 후진 방향으로 반복수행
( = 리프노드가 아닌, 바로 전 높이에서 가장 오른쪽 노드를 의미함.
왜냐면, 리프노드는 downheap 할 것이 없으니깐 )
- 시작 노드 : 첨자 [n/2] 인 노드
| [ PseudoCode ] |
| ----------------------------- |
| |
| Alg buildHeap(A) |
| input array A of n Keys |
| output heap A of size n |
| |
| for i <- [n/2] downto 1 |
| downHeap(i,n) |
| |
| return |
| |
| ------------------------------ |
- 실질적 구현단계 ( 삽입식 힙 생성. C ver. )
| [ PseudoCode ] |
| ---------------------------------------------- |
| |
| Alg InsertItem(key) |
| input integer key array H |
| output array H |
| |
| n <- n + 1 |
| H[n] <- key |
| upHeap(n) |
| return |
| |
| ----------------------------------------------- |
| |
| Alg removeMax() |
| input array H |
| output array H |
| |
| key <- H[1] |
| H[1] = H[n] |
| n <- n + 1 |
| downHeap(1) |
| return key |
| |
| ------------------------------------------------ |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #include<stdio.h> |
| |
| int n = 0; |
| int arr[100]; |
| |
| voidinsertItem(int key); |
| intremoveMax(); |
| voidupHeap(int idx); |
| voiddownHeap(int idx); |
| voidprintHeap(); |
| |
| intmain(){ |
| int key; |
| char op; |
| |
| do |
| { |
| scanf("%c",&op); |
| switch (op) |
| { |
| case 'i': |
| scanf("%d", &key); |
| insertItem(key); |
| break; |
| case 'd': |
| printf("%d\n",removeMax()); |
| break; |
| case 'p': |
| printHeap(); |
| break; |
| } |
| getchar(); |
| } while (op != 'q'); |
| |
| return 0; |
| } |
| |
| voidinsertItem(int key){ |
| arr[n+1] = key; |
| upHeap(n+1); |
| n = n + 1; |
| printf("%d\n", 0); |
| } |
| intremoveMax(){ |
| int key = arr[1]; |
| arr[1] = arr[n]; |
| downHeap(1); |
| n = n - 1; |
| return key; |
| } |
| voidupHeap(int idx) |
| int max; |
| if (idx != 1) |
| { |
| if (arr[idx] >= arr[idx / 2]) |
| { |
| max = arr[idx]; |
| arr[idx] = arr[idx / 2]; |
| arr[idx / 2] = max; |
| upHeap(idx / 2); |
| } |
| } |
| } |
| voiddownHeap(int idx) |
| int greater,tmp; |
| |
| if (n >= idx * 2) |
| { |
| greater = idx * 2; |
| if (n >= idx * 2 + 1) |
| if (arr[idx * 2 + 1] > arr[greater]) |
| greater = idx * 2 + 1; |
| if (arr[idx] < arr[greater]) |
| { |
| tmp = arr[idx]; |
| arr[idx] = arr[greater]; |
| arr[greater] = tmp; |
| downHeap(greater); |
| } |
| } |
| } |
| voidprintHeap(){ |
| for (int i = 1; i <= n; i++) |
| printf(" %d", arr[i]); |
| printf("\n"); |
| } |
- 실질적 구현단계 ( 상향식 힙생성 (재귀,비재귀). C ver. )
| [ PseudoCode ] |
| ------------------------------------------------------------------- |
| |
| Alg rBuildHeap(i) |
| input integer i, array H |
| output array H |
| |
| if(i > n) |
| return |
| rBuildHeap(2i) |
| rBuildHeap(2i + 1) |
| downHeap(i) |
| return |
| |
| --------------------------------------------------------------------- |
| |
| Alg buildHeap() |
| input array H |
| output array H |
| |
| for i <- n/2 downto 1 |
| downHeap(i) |
| return |
| |
| --------------------------------------------------------------------- |
| |
| #include <stdio.h> |
| |
| int arr[100],n= 0; |
| void rBuildHeap(int idx); |
| void downHeap(int idx); |
| void printHeap(); |
| |
| int main() |
| { |
| int keyCount; |
| |
| scanf("%d", &keyCount); |
| for (int i = 1; i <= keyCount; i++) |
| scanf("%d", &arr[i]); |
| n = keyCount; |
| rBuildHeap(1); |
| for (int i = 1; i <= keyCount; i++) |
| printf(" %d", arr[i]); |
| return 0; |
| } |
| void rBuildHeap(int idx) |
| { |
| if (idx <= n) |
| { |
| if(idx*2 <= n) |
| rBuildHeap(idx * 2); |
| if(idx*2 + 1 <= n) |
| rBuildHeap(idx * 2 + 1); |
| } |
| downHeap(idx); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| void downHeap(int idx) |
| { |
| int greater, tmp; |
| |
| if (n >= idx * 2) |
| { |
| greater = idx * 2; |
| if (n >= idx * 2 + 1) |
| if (arr[idx * 2 + 1] > arr[greater]) |
| greater = idx * 2 + 1; |
| if (arr[idx] < arr[greater]) |
| { |
| tmp = arr[idx]; |
| arr[idx] = arr[greater]; |
| arr[greater] = tmp; |
| downHeap(greater); |
| } |
| } |
| } |
| void printHeap() |
| { |
| for (int i = 1; i <= n; i++) |
| printf(" %d", arr[i]); |
| printf("\n"); |
| } |
| |
| #include <stdio.h> |
| |
| int arr[100], n = 0; |
| |
| void buildHeap(); |
| void downHeap(int idx); |
| void printHeap(); |
| |
| int main() |
| { |
| int keyCount; |
| |
| scanf("%d", &keyCount); |
| for (int i = 1; i <= keyCount; i++) |
| scanf("%d", &arr[i]); |
| n = keyCount; |
| buildHeap(); |
| for (int i = 1; i <= keyCount; i++) |
| printf(" %d", arr[i]); |
| return 0; |
| } |
| |
| void buildHeap() |
| { |
| for (int i = n / 2; i >= 1; i--) |
| downHeap(i); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| void downHeap(int idx) |
| { |
| int greater,tmp; |
| if (n >= idx * 2) |
| { |
| greater = idx * 2; |
| if (n >= idx * 2 + 1) |
| if (arr[greater] < arr[idx * 2 + 1]) |
| greater = idx * 2 + 1; |
| if (arr[idx] < arr[greater]) |
| { |
| tmp = arr[idx]; |
| arr[idx] = arr[greater]; |
| arr[greater] = tmp; |
| downHeap(greater); |
| } |
| } |
| } |
| void printHeap() |
| { |
| for (int i = 1; i <= n; i++) |
| printf(" %d", arr[i]); |
| printf("\n"); |
| } |