1ilsang

Developer who will be a Legend

Heap 의 특성을 살펴보고 Heap sort 를 구현해보자.

2019-10-21 1ilsangData-Structure & Algorithm

Heap image

힙은 최댓값/최소값을 빠르게 찾아내기 위해 고안된 ‘완전이진트리’ 자료구조이다.

여기서 중요한 점은 최대/최소의 값을 ‘검색’하는데 O(1) 의 시간복잡도를 가진다는 점이다. 이때 값을 얻기 위해 pop을 하게 되면 O(logN)의 시간이 걸린다. 또한 맨 처음 펼쳐진 N개의 값들을 힙에 세팅해 주어야 하므로 N 의 시간이 더 걸리게 된다. 그렇기 때문에 힙의 시간복잡도는 O(NlogN)이라고 이야기 한다.

요약하자면 힙 트리 세팅을 하기위해 N 개의 원소를 다 넣어야하므로 N 의 시간이 걸리고, 이때마다 노드가 이진트리를 타게되므로 절반씩 값들을 걸러 움직여(logN 의 시간) 총 NlogN 의 시간이 걸리게 된다.

즉 굉장히 빠르고 효율적인 자료구조이다.

힙을 이용하는 경우는 무궁무진한데, 양수 가중치 최단거리를 구하는 다익스트라 알고리즘을 구현할 때 우선순위큐를 사용하면 훨씬 효율적으로 다음 노드를 찾아갈 수 있게 된다. 여기서 잠깐! 갑자기 웬 우선순위큐? 우선순위큐는 Heap 으로 구현되어 있다!

위의 내용을 토대로 힙의 특성을 정리하면 아래와 같다.

  1. 완전이진트리이다.
  2. 자식은 반드시 부모보다 크거나 작다(최대/최소 힙)
  3. PriorityQueue 는 Heap 으로 구성되어 있다.
  4. O(NlogN) 의 시간복잡도를 가진다.

Heap image

힙을 구현하기 이전에 push, pop 연산에서 노드가 어떻게 움직이는지 미리 알아두고 가자.

Push

  1. 트리의 마지막 노드에 값을 넣는다.
  2. 부모 노드와 값을 비교한다.
  3. 루트에 도달하거나, 부모 노드보다 값이 작다면(최대힙기준) break 한다.
  4. 부모 노드보다 값이 크다면 부모 노드와 swap 한다.
  5. 재귀적으로 다시 부모 노드와 비교한다.(2번부터 다시 시작)

Pop

  1. 트리의 루트값을 저장한다.(return 용)
  2. 트리의 마지막 노드를 루트로 올린다.
  3. 왼쪽 자식노드, 오른쪽 자식노드, 자신 3개의 값을 비교한다.
  4. 자신이 가장 크다면(최대힙기준) break 한다.
  5. 아니라면 가장 큰 값과 swap 한다.
  6. 재귀적으로 다시 왼쪽, 오른쪽 자식노드와 비교한다.(3번부터 다시 시작)

구현은 위의 로직을 그대로 코드로 옮기면 된다.

public class MyHeap {
    private int[] arr;
    private int idx;

    MyHeap(int size) {
        idx = 0;
        arr = new int[size + 1];
    }

    public boolean push(int value) {
        if(idx + 1 >= arr.length) return false;

        arr[++idx] = value;

        int curIdx = idx;
        while(curIdx > 1) {
            int parentIdx = curIdx >> 1;
            if(arr[parentIdx] >= arr[curIdx]) break;

            int tmp = arr[parentIdx];
            arr[parentIdx] = arr[curIdx];
            arr[curIdx] = tmp;
            curIdx >>= 1;
        }

        return true;
    }

    public int pop() {
        if(idx < 1) return -1;
        int ret = arr[1];
        int curIdx = 1;

        arr[1] = arr[idx];
        arr[idx--] = 0;

        while(curIdx <= idx) {
            int tmpIdx = curIdx;
            if(curIdx * 2 <= idx && arr[tmpIdx] < arr[curIdx * 2]) tmpIdx = curIdx * 2;
            if(curIdx * 2 + 1 <= idx && arr[tmpIdx] < arr[curIdx * 2 + 1]) tmpIdx = curIdx * 2 + 1;
            if(curIdx == tmpIdx) break;

            int tmp = arr[tmpIdx];
            arr[tmpIdx] = arr[curIdx];
            arr[curIdx] = tmp;
            curIdx = tmpIdx;
        }

        return ret;
    }
}


위에서 정리한 push/pop flow 에 따라 그대로 구현한 코드이다. 천천히 따라가면서 읽으면 큰 무리 없을 것이라 본다.

그럼 이제 힙소트를 구현해보자.

class Main {
    public static void main(String[] args) {
        MyHeap heap = new MyHeap(10);
        int[] list = {1, 2, 3, 4, 5, 6, 7, 8};

        for (int i = 0; i < list.length; i++) {
            heap.push(list[i]);
        }

        for (int i = 0; i < list.length; i++) {
            System.out.printf(heap.pop() + " ");
        } // 8 7 6 5 4 3 2 1
    }
}


힙을 구현하면 힙 소트는 push/pop 연산으로 자연스럽게 가능한 것을 볼 수 있다.

이상으로 힙을 마무리 하려고 한다. 다음에는 문자열 알고리즘으로 돌아와야지!!

그럼 이만!