Heap의 사전적 의미는 무엇인가 차곡차곡 쌓여있는 더미
를 의미한다. 이를 통해, 자료 구조에서 힙이란 모래 더미처럼 삼각형 구조로 쌓여있는 구조임을 추측할 수 있다.
- 메모리 영역 Heap과 자료구조 Heap은 서로 상관관계가 없다. 메모리 영역 Stack은 자료구조 Stack를 사용하기에 Stack이라고 불리는 것과 달리, 메모리 영역 Heap은 자료구조 Heap을 사용하지 않는다. 단지, 영단어 힙(heap)은 '무엇인가 차곡차곡 쌓여있는 더미'라는 뜻을 지니고 있어, 사용 가능한 메모리 풀을 관용적으로 ‘Heap’이라고 불렀다고 한다.
힙(Heap)도 트리 자료 구조의 일종이다. 힙이 다른 트리와는 달리 데이터가 한 쪽으로 쏠리지 않고 모래 더미처럼 삼각형의 형태를 유지할 수 있는 이유는, 부모 노드가 항상 자식 노드들보다 크거나(최대이진힙) 작아야(최소이진힙) 한다는 규칙이 있고 형제 노드들 간의 관계에는 그러한 규칙이 없기 때문이다.
최대 이진힙(MaxBinaryHeap)에서는 부모 노드가 항상 자식 노드보다 커야한다.
최소 이진힙(MinBinaryHeap)에서는 부모 노드가 항상 자식 노드보다 작아야한다.
이진 힙에서 부모와 자식 노드 간의 관계는 위와 같이 크거나 작아야 하지만, 형제 노드들 간의 관계에는 그러한 규칙이 없다.
이진 힙은 언제가 가능한 가장 적은 최적의 공간
만을 차지한다.
이진 힙은 자료구조에서 자주 사용되는 우선순위 큐(Priority Queue)
나 그래프 순회 알고리즘
등에서 자주 사용된다
class MaxBinaryHeap {
constructor() {
this.values = [];
}
}
로직
1. 제일 밑에 요소를 넣는다. this.values.push(element);
2. 그 요소가 알아서 제 자리로 가도록 비눗방울처럼 띄어 올린다. this.bubbleUp();
아래 이미지는 [41,39,33,18,27,12] 이진 힙 데이터에 55를 Insert했을 때 발생하는 과정을 시각화한 것이다.
코드
insert(element){
//삽입하려는 요소를 배열 끝에 push
this.value.push(element);
//BubbleUp()메소드를 호출하여 올바른 위치로 정렬할 예정
this.BubbleUp();
}
BubbleUp(){
var idx = this.value.length - 1; //들어온 값의 인덱스
const element = this.value[idx] //들어온 값
//인덱스가 0이 되면 그것이 루트라 더이상 비교할 것이 없다.
while(idx > 0){
//자식 인덱스를 사용하여 부모인덱스 찾기
var parentIdx = ~~((idx-1)/2);
//변수에 부모 값 저장
var parent = this.value[parentIdx];
// 부모보다 작으면 루프 종료
if(element <= parent) break;
// 부모보다 크면 스왑해준 뒤 그 요소를 다시 자식으로 해서 다시 루프 반복
this.value[parentIdx] = element;
this.value[idx] = parent;
idx = parentIdx;
}
}
최대이진힙에서 root(가장 큰 값)를 제거하는 메서드다. 이진힙을 기반으로 한 우선순위 큐에서 최우선순위를 가져오고 제거할 때 사용하는 알고리즘이다.
메소드 로직
1. root를 제거한다.
2. root 자리에 가장 마지막 요소를 옮겨 넣는다.
3. root 자리에 옮겨진 요소가 제자리를 찾아서 내려가도록 조절한다.
아래 이미지는 [41,39,33,18,27,12] 이진 힙에서 ExtractMax()를 했을 때, 발생하는 과정을 시각화한 것이다.
1. 먼저 41을 제거하고 이를 나중에 리턴한다.
2. 가장 최근에 추가된(가장 뒤에 있는) 12를 root 자리로 옮겨 넣는다.
3. root 자리에 올라선 12가 제자리로 돌아가기 위해서 child 값과 비교하고 child 값 중 더 큰 것과 자리를 바꾼다.
4. 한 단계 아래로 내려간 12가 다시 그 아래 child 중 값이 큰 녀석인 27과 크기를 비교하고, 12가 더 작으므로 12와 27이 자리를 서로 바꾼다.
5. 12 아래에 더 이상 하위 child가 없으므로 반복문 종료하고, 제거했던 41을 return한다.
extractMax(){
var max = this.value[0]; //루트값이 가장 큰 값이다
var end = this.value.pop();//배열 가장 마지막 요소
//pop()한 후 배열에 요소가 남아있으면
if(this.value.length > 0){
//root에 가장 마지막 요소 end를 넣고
this.value[0] = end;
//제자리를 찾아가도록 sinkDown() 호출
this.sinkDown();
}
//가장 큰 요소를 반환
return max;
}
sinkDown(){
var idx = 0;
var element = this.value[idx];
while(true){
//부모 인덱스로부터 자식 인덱스를 찾는 공식 이용
let leftchildIdx = idx * 2 + 1;
let rightchildIdx = idx * 2 + 2;
var leftchild,rightchild;
var swap = null;
//left 인덱스가 배열 길이보다 작으면
if(leftchildIdx < this.value.length){
leftchild = this.value[leftchildIdx];
if(leftchild > element){
swap = leftchildIdx;
}
}
//right 인덱스가 배열 길이보다 작으면
if(rightchildIdx < this.value.length){
rightchild = this.value[rightchildIdx];
//왼쪽에서 스왑이 발생하지 않을 수 있으므로 발생하지 않고
// 비교요소보다 right가 더 큰지 비교 or 왼쪽요소도 비교요소보다는 크지만
//right 자식보다는 작으면 swap을 오른쪽 값으로 바꿈
if((swap === null && element < rightchild)
|| (swap !== null && leftchild < rightchild)){
swap = rightchildIdx;
}
}
if(swap === null) break;
//선정된 swap 인덱스로 요소들을 교환한 뒤 재할당 후 자식요소 찾기로 다시 반복
this.value[idx] = this.value[swap];
this.value[swap] = element;
idx = swap;
}
}
최소이진힙
으로 구현하고 value 외에도 priority
(우선순위) 클래스를 함께 담기 위해 Node 클래스를 사용한다.최소 이진 힙
으로 구성했음을 주의하자!class Node{
constructor(val,priority){
this.val = val;
this.priority = priority; //우선순위가 추가됨
}
}
class PriorityQueue{
constructor(){
this.val = [];
}
enqueue(val,priority){ //insert에서 큐에 쓰이는 enqueue로 변경
let newNode = new Node(val,priority); //Node는 값과 우선순위를 가지고 있음
this.val.push(newNode);
this.BubbleUp();
}
BubbleUp(){
var idx = this.val.length - 1;
const element = this.val[idx];
while(idx > 0){
var parentIdx = Math.floor((idx-1)/2);
var parent = this.val[parentIdx];
//값을 비교하는게 아니라 값이 가진 우선순위로 비교
//우선순위는 1,2,3,4 처럼 낮은 숫자가 가장 크다
if(element.priority >= parent.priority) break;
this.val[parentIdx] = element;
this.val[idx] = parent;
idx = parentIdx;
}
}
dequeue(){ //extractMax(최댓값 반환)을 dequeue로 변경
var min = this.val[0];
const end = this.val.pop();
if(this.val.length > 0){
this.val[0] = end;
this.Sinkdown();
}
return min;
}
Sinkdown(){
var idx = 0;
var element = this.val[idx];
while(true){
let leftchildIdx = idx * 2 + 1;
let rightchildIdx = idx * 2 + 2;
var leftchild,rightchild;
var swap = null;
//마찬가지로 로직은 비슷하나 값의 우선순위로 비교한다
if(leftchildIdx < this.val.length){
leftchild = this.val[leftchildIdx];
if(leftchild.priority < element.priority){
swap = leftchildIdx;
}
}
if(rightchildIdx < this.val.length){
rightchild = this.val[rightchildIdx];
if((rightchild.priority < element.priority && swap === null)||
(swap !== null && rightchild.priority < leftchild.priority)){
swap = rightchildIdx;
}
}
if(swap === null) break;
this.val[idx] = this.val[swap];
this.val[swap] = element;
idx = swap;
}
}
}
BST
와 달리 왼쪽 오른쪽을 다 채워야 다음으로 넘어갈 수 있다.