힙(Heap)이란?

힙(Heap)은 특성을 만족하는 완전 이진 트리 기반의 자료구조다. 부모노드의 값은 항상 자식 노드의 값보다 크거나(Max heap, 최대 힙) 작아야(Min heap, 최소 힙) 한다. 따라서 루트 노드의 값은 힙 트리의 최소값이거나 최대값이다. 이진트리 기반으로 하는 이유는 다른 구조의 m-ary tree를 사용해도 얻는 이득이 없기 때문이다.

  • 힙은 정렬된 구조가 아니다! 따라서 heap에서 자료를 꺼낼 때는 큐처럼 차례로 꺼내야 정렬된 형태의 배열을 얻을 수 있다.
  • 힙 정렬, 다익스트라 알고리즘, 최소 신장 트리 등에 사용된다.

힙(Heap) 구현

힙 선언

class BinaryHeap:
    def __init__(self):
        self.items = [None]

    def __len__(self):
        return len(self.items) - 1    

0번 인덱스를 사용하지 않는 이유는 부모 노드의 각 자식 노드는 부모 노드의 좌표 i에 대해 2i, 2i + 1로 주로 표현되는데 부모 노드의 좌표가 0이 되어버리면 양 자식의 노드 좌표도 0이 되어 탐색이 진행되지 않기 때문이다.

값 삽입

Data Structure_Heap_001

값 삽입은 다음의 순서로 진행된다.

  • 삽입될 값은 가장 하위레벨의 최대한 왼쪽에 삽입한다. (가장 끝 자리에 삽입)
  • 삽입된 노드와 부모노드의 값과 비교한다.
  • 규칙이 성립하면 그대로 두고 위배되면 부모 노드와 값을 바꾼다.
  • 이후 규칙이 위배되지 않을 때까지 반복한다.
    def insert(self, val):
        self.items.append(val)
        self._precolate_up()

    def _precolate_up(self):
        i = len(self)
        p = i // 2
        while p > 0:
            if self.items[i] < self.items[p]: # 최소 힙 조건
                self.items[i], self.items[p] = self.items[p], self.items[i]
            #if self.items[i] > self.items[p] # 최대 힙 조건
            i = p
            p = i // 2

시간 복잡도는 높이 h만큼이므로 $O(h) = O(log\ n)$이다.

값 추출

루트를 추출하면 힙의 성질에 따라 최소값이나 최대값이 추출된다. 그러나 힙은 정렬된 구조가 아니기 때문에 다시 힙의 성질에 맞춰 재배열하는 "Heapify" 과정이 필요하다.

Data Structure_Heap_002

    def extract(self):
        extracted = self.items[1]
        self.items[1] = self.items[len(self)]
        self.items.pop()
        self._precolate_down(1)
        return extracted

    def _precolate_down(self, i):
        l, r = 2 * i, 2 * i + 1
        t = i
        # 최대 힙 조건
        # if l <= len(self) and self.items[l] > self.items[target]
        # 최소 힙 조건
        if l <= len(self) and self.items[l] < self.items[t]:
            t = l # 좌측 노드에 대해 검사
        if r <= len(self) and self.items[r] < self.items[t]:
            t = r # 우측 노드에 대해서도 검사
        if t != i: # swap이 요구된다면
            self.items[i], self.items[t] = self.items[t], self.items[i]
            self._precolate_down(t)

전체 코드

class BinaryHeap:
    def __init__(self):
        self.items = [None]

    def __len__(self):
        return len(self.items) - 1

    def insert(self, val):
        self.items.append(val)
        self._precolate_up()

    def _precolate_up(self):
        i = len(self)
        p = i // 2
        while p > 0:
            if self.items[i] < self.items[p]:  # 최소 힙 조건
                self.items[i], self.items[p] = self.items[p], self.items[i]
            # if self.items[i] > self.items[p] # 최대 힙 조건
            i = p
            p = i // 2

    def extract(self):
        extracted = self.items[1]
        self.items[1] = self.items[len(self)]
        self.items.pop()
        self._precolate_down(1)
        return extracted

    def _precolate_down(self, i):
        l, r = 2 * i, 2 * i + 1
        t = i
        # 최대 힙 조건
        # if l <= len(self) and self.items[l] > self.items[target]
        # 최소 힙 조건
        if l <= len(self) and self.items[l] < self.items[t]:
            t = l # 좌측 노드에 대해 검사
        if r <= len(self) and self.items[r] < self.items[t]:
            t = r # 우측 노드에 대해서도 검사
        if t != i: # swap이 요구된다면
            self.items[i], self.items[t] = self.items[t], self.items[i]
            self._precolate_down(t)

heap = BinaryHeap()
nums = [2, 3, 5, 4, 6, 10, 11, 8, 9, 7, 1]
for n in nums:
    heap.insert(n)
result = []
for _ in range(len(nums)):
    result.append(heap.extract())
print(result)

Data Structure_Heap_003

heap을 빌드하는 과정에서의 엄밀한 시간 복잡도

일반적으로 삽입 과정의 시간 복잡도를 $O(h) = O(log\ n)$이니까 1부터 무한히 발산하는 n에 대해 heap을 build한다고 가정할 때 그 과정의 시간복잡도를 단순히 n을 곱해서 $O(nlog\ n)$이라 표현할 수 있다. 물론 $O(nlog\ n)$보다야 작겠지만 좀 더 엄밀하게 계산하면 $O(2n) = O(n)$이다. 이에 대한 증명 과정은 예전에 작성했던 참고포스트 - Analysis of Algorithms | Time Complexity of building a heap좀 더 엄밀하게 계산한 BUILD-HEAP 함수의 시간 복잡도 항목에서 찾을 수 있다.

+ Recent posts