Computer Science/자료구조

Priority Queue & Heap

Chavo Kim 2020. 10. 21. 23:20

1. Priority Queue

1.1 문제 상황

삽입은 자유롭게 이루어지지만, element를 꺼낼 때 항상 가장 큰 element(removemax)를 꺼내고자 한다면 가장 좋은 자료구조는 무엇인가?

 

ex) multi-tasking operating system에서 scheduling job하는 문제!

 

1.2 Unsorted array or linked list

insert : O(1)의 시간 소요

removemax : max인 element를 찾기 위해 O(n)의 시간 소요

 

1.3 Sorted array of linked list

insert : O(n)의 시간 소요, sorting을 위해

removemax : 맨끝 값을 return하면 되니까 O(1)

 

하지만 이때 heap 자료구조를 사용하게 되면 insert와 removemax 모두 O(logn)에 가능하다!

 

2. Heap

2-1. 정의

complete binary tree이며, 두가지 종류로 나뉘어진다.

 

Min-heap : 모든 부모 노드는 자식 노드보다 작다.

Max-heap : 모든 부모 노드는 자식 노드보다 크다.

 

Binary Search Tree와 다르게 partially ordered되어 있다.

-> max heap의 경우, parent는 child보다 크지만 child끼리는 따로 ordered 되어 있지 않음.

-> BST의 경우, 왼쪽 자식, 부모 노드, 오른쪽 자식이 각각 크기에 따른 순서가 정해져 있음(completely ordered)

 

Heap의 경우 보통 array-based complete binary tree로 구현된다!! 

 

max heap

2-2. Max Heap Property

positions of leaf nodes in a max heap with n nodes : floor($\frac{n}{2}$)

 

그래서 max heap에서는 n - floor($\frac{n}{2}$)의 leaf nodes를 가진다.

 

왜?

-> index로 생각해보면 편함

 

2-3. Max Heap Implementation

boolean isLeap(int pos){ return (pos >= n/2) && (pos < n); }

int leftchild(int pos){

    if(pos >= n/2)

        return null;

   return 2*pos + 1;

 }

int rightchild(int pos){

    if(pos >= (n-1)/2)

       return null;

    return 2*pos + 2;

}

int parent(int pos){

     if(pos == 0)

        return null

     return (pos-1)/2 ;

}

 

대부분 간단히 구현 가능한 메소드(array-based)!

 

2-4. Building Heaps(sift down)

binary tree에서 Heaps 구조로 만들 때

 

sift down method를 사용한다.

 

귀납법을 이용하는 방법인데,

 

sift down method

 

이미 left subtree와 right subtree에 heap이 구현되어 있다고 생각하고 R을 적절한 위치에 놓는거다.

 

만약

1. R이 H1, H2보다 크다면? 움직일 필요 없음!

2. R이 H1, H2보다 작다면? max(H1, H2)를 R과 자리를 바꾼다. => 그래야 양 자식들이 부모 노드보다 작게 됨!

 

이를 자식 노드로 이어가면서 계속 반복한다.

 

public void buildheap(){
	//부모인 노드를 대상으로 siftdown를 수행
	for (int i = n/2-1; i>=0; i--) siftdown(i);
}

private void siftdown(int pos){
	assert (pos >= 0) && (pos < n) :
    	"Illegal heap position";
    while(!isLeaf(pos)) {
    	int j = leftChild(pos);
        //오른쪽 자식 노드가 더 크면 오른쪽 자식 노드로 j 변경
        if((j<(n-1)) && (Heap[j].compareTo(Heap[j+1]) < 0))
        	j++;
        //root node가 자식 노드들보다 크거나 같으면 종료
        if(Heap[pos].compareTo(Heap[j]) >= 0){
        	return;
        }
        //root node가 자식 노드보다 작으면 swap
        DSutil.swap(Heap, pos, j);
        //pos 위치가 j로..
        pos = j;
        //pos가 제자리를 찾거나 leaf에 갈 때까지 반복
}

 

2-5. RemoveMax, Insert

public E removemax() {
	if(n == 0)
    	System.out.println("Removing from empty heap");
    //root node를 Heap[n]으로 보냄.
    DSutil.swap(Heap, 0, --n);
    if(n != 0) siftdown(0);
    return Heap[n];
}

public void insert(E val){
	asssert n < size:
		"Heap is full";
    int curr = n++;
    Heap curr = val;
    //Siftup until curr parent's key > curr key
    while((curr != 0) && Heap[curr].compareTo(Heap[parent(curr)] > 0)){
    	DSutil.swap(Heap, curr, parent(curr));
        curr = parent(curr);
    }
}

Removemax의 경우 worstcase가 바꾼 n node가 그대로 leaf로 떨어지는 경우이다.

$\theta(logn)$이 걸림 => depth의 크기

 

Insert 시에 n개의 value를 root에 넣는다면 총 $\theta(nlogn)$의 시간이 걸리겠지만

가장 아래의 node에 넣는다면 대부분의 node들이 아래에 몰려있기 때문에($2^h$) 많은 거리를 움직이지 않게 된다.

i를 1로 시작하는 바닥부터 세는 레벨이라고 할 때

 

$$\sum_{i = 1}^{\log n} (i - 1) \frac{n}{2^i} = \frac{n}{2} \sum_{i-1}^{\log n} \frac{i - 1}{2 ^ {i-1}} = \theta(n)$$

 

$$\sum_{i = 1}^n \frac{i}{2^i} = 2 - \frac{n+2}{2^n}$$

 

After insert and removemax, Heap remains as complete binary tree.