힙(Heap)이란 무엇인가

2020.03.12

메인

힙(Heap)이란?

힙 트리(Heap Tree), 보통 줄여서 힙(Heap)이라고 부른다.

힙은 부모 노드의 값이 항상 하위 노드의 값보다 작거나 큰 구조의 트리이다.

트리의 일종으로 완전 이진 트리(트리 문서 참고)이면서, 부모 노드의 값이 항상 하위 노드의 값보다 작거나 큰 구조의 트리이다.

힙의 사전적인 의미가 '쌓아 올린 더미'인 것 처럼, 데이터가 위에서부터 규칙적으로 쌓여있는 구조이다.

데이터가 정렬되는 방식에 따라 최대 힙(Max Heap), 최소 힙(Min Heap)으로 구분된다.

힙은 노드를 정렬하는 방식에 따라 최대 힙(Max Heap), 최소 힙(Min Heap)으로 나뉜다.

최대 힙 Max Heap

부모 노드의 값이 항상 하위 노드의 값보다 크다.

최소 힙 Min Heap

부모 노드의 값이 항상 하위 노드의 값보다 작다.

힙은 우선 순위 큐를 구현하는 가장 효율적인 자료구조이다.

최소값/최대값이 항상 배열의 첫번째 요소에 저장되므로 쉽고 빠르게(O(1)시간) 최소값/최대값을 찾을 수 있다.

삽입/삭제는 루트 노드에 대해서만 가능하지만 O(log n)으로 매우 빠르다.

이런 특성은 우선 순위 큐(큐 문서 참고)를 구현하는데 매우 유리하여, 구현하는데 가장 효율적인 자료구조로 알려져있다.

인터페이스

  • insert(key) - 노드를 추가한다.
  • remove() - root노드를 삭제한다.(큐 자료구조의 pop()과 유사)

구현 - JavaScript에서의 힙

힙은 완전 이진 트리이므로 배열로 효율적으로 구현할 수 있다.

배열의 index와 노드의 위치를 맵핑해서 사용한다.

깊이는 위에서 아래로, 왼쪽에서 오른쪽 방향으로 차례대로 index가 할당된다.

규칙적으로 index가 할당되므로 아래와 같은 규칙성이 있다.

  • 왼쪽 자식의 index: 부모 idnex * 2
  • 오른쪽 자식의 index = 부모 idnex * 2 + 1
  • 부모의 인덱스 = 자식 index / 2 (몫만 사용)

최대힙 Max Heap

class MaxHeapTree {
  // 편리한 구현을 위해 index는 1부터 사용
  constructor() {
    this._items = [""];
  }

  get items() {
    return this._items;
  }

  get root() {
    return _items[1];
  }

  /**
   * 구현 절차
   * - 1) 힙의 가장 마지막 위치(배열의 마지막)에 노드에 추가한다
   * - 2) 힙의 조건을 만족시키도록 연산 (부모노드와 교환)
   *  - 새로 추가한 노드와 부모 노드를 비교하여 부모노드가 더 작으면 위치를 바꾼다
   *  - (더 큰 부모노드가 없을때까지 반복)
   */
  insert(key) {
    this._items.push(key);
    this._allignAfterInsert();
  }

  /**
   * 힙의 삭제는 루트 노드 삭제만 구현한다. (queue의 pop 연산이라고 생각하면 됨)
   *
   * 구현 절차
   * - 1) 루트노드를 제거
   * - 2) 제거한 루트노드자리에 마지막 노드를 삽입
   * - 3) 힙의 조건을 만족시키도록 연산
   *  - 루트노드의 자식노드 중 더 큰 값과 교환한다.
   *  - (더 큰 자식노드가 없을때까지 반복)
   */
  remove() {
    this._items[1] = this._items.pop();
    this._allignAfterRemove();
  }

  _allignAfterInsert() {
    let childIndex = this._items.length - 1;
    let child = this._items[childIndex];

    let parentIndex = Math.floor(childIndex / 2) || 1;
    let parent = this._items[parentIndex];

    let newChildIndex;
    while (parent < child) {
      newChildIndex = parentIndex;
      this._items[newChildIndex] = child;
      this._items[childIndex] = parent;

      parentIndex = Math.floor(newChildIndex / 2) || 1;
      parent = this._items[parentIndex];
      childIndex = newChildIndex;
    }
  }

  _allignAfterRemove() {
    let parentIndex = 1;
    let parent = this._items[parentIndex];

    let leftChild = this._items[parentIndex * 2];
    let rightChild = this._items[parentIndex * 2 + 1];

    let bigChild = leftChild > rightChild ? leftChild : rightChild;
    let bigChildIndex = this._items.indexOf(bigChild);

    let newParentIndex;
    while (parent < bigChild) {
      newParentIndex = bigChildIndex;
      this._items[newParentIndex] = parent;
      this._items[parentIndex] = bigChild;

      leftChild = this._items[newParentIndex * 2];
      rightChild = this._items[newParentIndex * 2 + 1];

      bigChild = leftChild > rightChild ? leftChild : rightChild;
      bigChildIndex = this._items.indexOf(bigChild);
      parentIndex = newParentIndex;
    }
  }
}

// Test
it("MaxHeapTree 클래스는 잘 동작한다.", () => {
  const heap = new MaxHeapTree();

  heap.insert(9);
  heap.insert(3);
  heap.insert(5);
  heap.insert(10);
  heap.insert(6);
  expect(heap.items).toEqual(["", 10, 9, 5, 3, 6]);

  heap.insert(8);
  expect(heap.items).toEqual(["", 10, 9, 8, 3, 6, 5]);

  heap.remove();
  expect(heap.items).toEqual(["", 9, 6, 8, 3, 5]);
  heap.remove();
  expect(heap.items).toEqual(["", 8, 6, 5, 3]);
});

최소힙 Min Heap

class MinHeapTree {
  // 편리한 구현을 위해 index는 1부터 사용
  constructor() {
    this._items = [""];
  }

  get items() {
    return this._items;
  }

  get root() {
    return _items[1];
  }

  /**
   * 구현 절차
   * - 1) 힙의 가장 마지막 위치(배열의 마지막)에 노드에 추가한다
   * - 2) 힙의 조건을 만족시키도록 연산 (부모노드와 교환)
   *  - 새로 추가한 노드와 부모 노드를 비교하여 부모노드가 더 크면 위치를 바꾼다
   *  - (더 큰 부모노드가 없을때까지 반복)
   */
  insert(key) {
    this._items.push(key);
    this._allignAfterInsert();
  }

  /**
   * 힙의 삭제는 루트 노드 삭제만 구현한다. (queue의 pop 연산이라고 생각하면 됨)
   *
   * 구현 절차
   * - 1) 루트노드를 제거
   * - 2) 제거한 루트노드자리에 마지막 노드를 삽입
   * - 3) 힙의 조건을 만족시키도록 연산
   *  - 루트노드의 자식노드 중 더 작은 값과 교환한다.
   *  - (더 작은 자식노드가 없을때까지 반복)
   */
  remove() {
    this._items[1] = this._items.pop();
    this._allignAfterRemove();
  }

  _allignAfterInsert() {
    let childIndex = this._items.length - 1;
    let child = this._items[childIndex];

    let parentIndex = Math.floor(childIndex / 2) || 1;
    let parent = this._items[parentIndex];

    let newChildIndex;
    while (parent > child) {
      newChildIndex = parentIndex;
      this._items[newChildIndex] = child;
      this._items[childIndex] = parent;

      parentIndex = Math.floor(newChildIndex / 2) || 1;
      parent = this._items[parentIndex];
      childIndex = newChildIndex;
    }
  }

  _allignAfterRemove() {
    let parentIndex = 1;
    let parent = this._items[parentIndex];

    let leftChild = this._items[parentIndex * 2];
    let rightChild = this._items[parentIndex * 2 + 1];

    let smallChild = leftChild < rightChild ? leftChild : rightChild;
    let smallChildIndex = this._items.indexOf(smallChild);

    let newParentIndex;
    while (parent > smallChild) {
      newParentIndex = smallChildIndex;
      this._items[newParentIndex] = parent;
      this._items[parentIndex] = smallChild;

      leftChild = this._items[newParentIndex * 2];
      rightChild = this._items[newParentIndex * 2 + 1];

      smallChild = leftChild < rightChild ? leftChild : rightChild;
      smallChildIndex = this._items.indexOf(smallChild);
      parentIndex = newParentIndex;
    }
  }
}

// Test
it("MinHeapTree 클래스는 잘 동작한다.", () => {
  const heap = new MinHeapTree();

  heap.insert(9);
  heap.insert(3);
  heap.insert(5);
  heap.insert(10);
  heap.insert(6);
  expect(heap.items).toEqual(["", 3, 6, 5, 10, 9]);

  heap.insert(2);
  expect(heap.items).toEqual(["", 2, 6, 3, 10, 9, 5]);

  heap.remove();
  expect(heap.items).toEqual(["", 3, 6, 5, 10, 9]);
  heap.remove();
  expect(heap.items).toEqual(["", 5, 6, 9, 10]);
});

활용 사례

  • 우선순위 큐 구현 (가장효율적인 우선순위 큐 구현 자료구조)
  • 무손실 압축 알고리즘인 허프만 코드
  • 아디트야 바르가바, 『Hello Coding 그림으로 개념을 이해하는 알고리즘』, 김도형, 한빛미디어(2017)
  • 스기우라 켄, 『그림으로 배우는 알고리즘』, 서재원, 영진(2016)
  • 이소 히로시, 『모던 자바스크립트 입문』, 서재원, 길벗(2018)
  • Martin Erwig, 『그들은 알고리즘을 알았을까?』, 송원형, 영진(2017)
  • Loiane Groner, 『자바스크립트 자료 구조와 알고리즘』, 이일웅, PACKT-에이콘(2015)
글쓴이쿠스, Qus
직업은 소프트웨어 개발자.
기술로 사회문제를 해결하는 시빅해킹(Civic Hacking)에 관심이 많고,
이것저것 생각나는 것들을 글로 정리하는 것을 좋아합니다.