Heap

|

Heap 이란

부모 노드의 값은 자식 노드의 값보다 항상 크다(또는 작다)라는 규칙을 갖고 있는 트리(Tree) 형태의 자료 구조를 Heap 이라고 합니다.

크게 Max Heap과 Min Heap으로 나누어집니다. Max Heap은 부모 노드의 값이 항상 자식 노드의 값보다 큰 규칙을 갖고 있으며, Min Heap은 그 반대입니다.


image

Heap의 성격상 최상위 노드인 root 노드에는 데이터 중에서 가장 큰(작은) 값이 저장됩니다. 즉, Heap에서 데이터를 하나씩 꺼내게 되면 정렬과 같은 효과를 가지는데, 이러한 정렬을 힙 정렬(Heap Sort)라고 합니다. 성능은 O(N log N)입니다.


특징

Heap은 이진 트리(Binary Tree) 형태로 이루어져 있습니다. 그리고 이진 트리 중에서도 완전 이진 트리(Complete Binary Tree)입니다.

완전 이진 트리라는 특징(앞에서부터 차례대로 채워지는 규칙) 때문에 Heap을 구현할 때,  트리보다 배열을 이용하는 경우가 많습니다. 위에서 예제로 그린 트리를 배열로 나타내면 다음과 같습니다.

image

배열 칸에 값이 들어가며, 위쪽에 있는 숫자는 배열 칸의 인덱스입니다. 배열은 0부터 시작해도 되고, 1부터 시작해도 됩니다. 다만, 그 공식이 살짝 바뀌긴 하는데 큰 차이는 없습니다. 저같은 경우는 그냥 1부터 시작하는게 습관이 되어서 주로 0번째 배열은 1번째 배열부터 채워나가는 편입니다.

배열 칸의 인덱스와 트리안의 노드들과의 관계는 다음과 같습니다.

image


이제 부모 노드와 자식 노드간의 관계를 유추할 수 있습니다. 그 관계는 다음과 같습니다.

image

자식 노드의 인덱스를 2로 나누면 부모 노드의 인덱스가 되고, 반대로 부모 노드에서 왼쪽 자식 노드는 2를 곱한 값, 오른쪽 자식 노드는 2를 곱한 값에 1을 더한 값을 인덱스로 가지게 됩니다. (배열의 시작을 0으로 하느냐, 1로 하느냐에 따라 이 공식은 달라집니다.)

  • 부모 노드 인덱스 = (자식 노드 인덱스) / 2
  • 왼쪽 자식 노드 인덱스 = (부모 노드 인덱스) x 2
  • 오른쪽 자식 노드 인덱스 = (부모 노드 인덱스) x 2 + 1


코드

Heap 정의

static const int MAX_HEAP_SIZE = 1000;
class Heap {
public:
    Heap() {
        size = 0;
    }

    // Heap의 함수는 데이터의 삽입/삭제 2개가 필요하다.
    void push(int value);
    int pop();

private:
    int data[MAX_HEAP_SIZE];
    int size;

    void swap(int pos1, int pos2) {
        int temp = data[pos1];
        data[pos1] = data[pos2];
        data[pos2] = temp;
    }
};


구현

// 데이터 삽입은 쉽다.
// 배열의 가장 마지막 칸에 데이터를 넣은 다음,
// 부모 노드들과 비교해나가면서 부모가 자식보다 값이 더 크면
// 교환을 해주면 된다.
void Heap::push(int value) {
    size++;
    data[size] = value;

    int pos = size;
    while (true) {
        if (pos == 1) break;

        int parent_idx = pos / 2;
        int parent_value = data[parent_idx];

        if (parent_value <= data[pos]) {
            break;
        }

        swap(parent_idx, pos);
        pos = parent_idx;
    }
}

// 데이터 삭제는 복잡하다.
// 배열의 첫번째 인덱스 값을 리턴하는 것은 단순하게 되지만
// 그러면서 Heap을 재구성하는 것이 조금 복잡하다.
// 현재 비어 있는 칸의 왼쪽 자식과 오른쪽 자식의 값을 비교해서
// 더 작은 값을 비어 있는 칸으로 옮기는 작업을 반복적으로
// 수행하면 된다.
int Heap::pop() {
    if (size <= 0) return -1;
    int ret = data[1];
    data[1] = data[size];
    size--;

    int pos = 1;
    while (true) {
        int left_child_idx = pos * 2;
        int right_child_idx = pos * 2 + 1;

        if (left_child_idx > size) break;

        // child node 가 1개인 경우와 2개인 경우를 나누어 처리
        if (right_child_idx > size) {
            int left_child_value = data[left_child_idx];
            if (left_child_value >= data[pos]) {
                break;
            }
            swap(left_child_idx, pos);
            pos = left_child_idx;
        }
        else {
            int left_child_value = data[left_child_idx];
            int right_child_value = data[right_child_idx];

            if ((left_child_value >= data[pos]) && (right_child_value >= data[pos])) {
                break;
            }

            if (left_child_value <= right_child_value) {
                swap(left_child_idx, pos);
                pos = left_child_idx;
            }
            else {
                swap(right_child_idx, pos);
                pos = right_child_idx;
            }
        }
    }

    return ret;
}


Heap을 쓰는 경우가 많지는 않지만 가끔씩 요긴하게 쓰일 때가 있습니다. 대표적으로 우선 순위 큐(Priority Queue)를 들 수 있겠네요. 우선 순위 큐는 최단 경로 찾기 알고리즘(ex. Dijkstra) 등에서 활용되고 있습니다.

우분투(Ubuntu)에서 deb, rpm 파일 설치하기

|

확장자가 deb이거나 또는 rpm인 파일은 리눅스에서 사용하는 프로그램 설치 패키지입니다. GUI 화면에서라면 더블 클릭이나 Installer 등을 연계하여 바로 설치할 수 있는데, 여기서는 터미널 명령어로 알아보도록 하겠습니다.


deb 파일

데비안 꾸러미 파일입니다. 우분투가 데비안 GNU/리눅스 배포판과 밀접한 관련이 있는데, deb 파일 설치는 터미널에서 dpkg 명령어와 -i 옵션을 이용해서 할 수 있습니다.

dpkg -i [패키지이름.deb]

라고 입력하면 됩니다.

설치 제거는 -r 옵션을 이용해서 할 수 있습니다.

dpkg -r [패키지이름.deb]


rpm 파일

rpm 파일의 경우는 레드햇 패키지 관리자에서 사용되는 파일인데, 우분투에서는 rpm 파일을 사용하는 것을 권하지는 않습니다.

대부분의 rpm 파일은 deb 파일로도 존재하기 때문에 deb 파일을 구해서 설치하는 것이 더 바람직하지만, 정 rpm 파일을 설치해야 할 일이 생긴다면 alien이라는 명령어를 이용해서 rpm 파일을 deb 파일로 변환할 수 있습니다.

alien [패키지이름.rpm]

만약 alien이 설치되어 있지 않으면,

sudo apt-get install alien

으로 설치하시면 됩니다.

chmod 명령어를 이용한 파일 권한 설정

|

보통 리눅스/유닉스에서 파일의 읽기/쓰기 등의 권한 설정을 할 때 chmod 명령어를 사용합니다. 보통 모든 권한을 줄 때 chmod 777 [이름]를 입력합니다.

리눅스에서 권한은 총 10자리로 표시됩니다. ls -al 명령어를 이용하여 다음과 같이 권한을 확인할 수 있습니다.

drwxr-xr-x 4 root root        4096 2011-03-12 14:58 .
drwxr-xr-x 7 root root        4096 2011-03-12 15:37 ..
drwxr-xr-x 2 root root        6364 2011-03-12 14:32 CSC
-r-xr-xr-x 1 root root        23110 2011-03-12 14:32 build.sh

여기서 맨 앞부분에 있는 drwxr-xr-x와 같은 부분이 권한 표시 부분입니다.

각각의 알파벳들은 다음과 같은 권한을 의미합니다.

  • d : 디렉토리 구분 (d이면 디렉토리, -이면 파일)
  • r : 읽기 권한
  • w : 쓰기 권한
  • x : 실행 권한

그리고 맨 앞자리는 디렉토리인지 아닌지 여부, 그 다음 각 3자리씩은 소유자 권한, 그룹 권한, 전체 권한을 표현하고 있습니다.

즉,

d rwx r-x r-x
디렉토리 소유자 권한 그룹 권한 전체 권한

를 나타냅니다.

리눅스에서 권한 설정은 chmod 명령어를 이용하면 변경할 수 있습니다. 예를 들어 chmod 777 snowdeer라고 할 때, 777이라는 값은 다음과 같은 규칙을 갖고 있습니다. 권한을 나타내는 rwx 각각은 r(4), w(2), x(1)의 값을 가지고 있습니다. 즉, r + w + x의 값은 7이 됩니다.

결국 777은 소유자, 그룹, 전체 권한을 모두 rwx로 바꾸겠다는 뜻입니다.

Counting Sort

|

카운팅 정렬(Counting Sort)는 O(N) 성능을 가지는 정렬 알고리즘입니다. 그리고 구현까지 간단해서 알아두면 좋습니다. 흔히 성능을 위해 많이 사용하는 Cache와 비슷한 성격을 갖고 있습니다. O(N) 성능이라고 하면, 그냥 데이터들을 한 번 읽고 지나가면 정렬이 되는 속도라고 볼 수 있습니다.

일반적으로 많이 쓰는 정렬 알고리즘들인 Quick Sort나 Merge Sort가 O(N log N)임을 생각하면 상당히 훌륭한 편입니다. 하지만, 이런 엄청난 속도임에도 정렬 알고리즘의 대표적인 예시가 되지 못하는 이유는 특수한 환경에서만 쓸 수 있는 알고리즘이기 때문입니다.

일단, 원리를 알아보도록 하겠습니다.


원리

Counting Sort의 원리는 다음과 같습니다.

카운팅하고자 하는 숫자 값중 가장 큰 값 만큼의 배열을 하나 만듭니다. 예를 들어서 정렬하고자 하는 값이

{ 5, 14, 2, 7, 11, 3, 6, 10, 9, 18 }

라고 하면, 이 중에서 가장 큰 값이 18이므로 18만큼의 배열을 하나 만듭니다. 넉넉하게 크기 20짜리 배열을 하나 만들어보겠습니다. 저는 아래 코드에서는 result 라는 배열로 만들었습니다. 그리고 데이터를 처음부터 읽으면서 그 값을 인덱스로 하는 칸에 +1 합니다. 예를 들어, 데이터가 숫자 5인 경우는 result[5]를 +1 해주면 됩니다.

이렇게 모든 데이터를 읽어 들인 후, 그 다음에는 result 배열을 처음부터 읽으면서 그 값이 0보다 큰 경우에만 출력을 해주면 됩니다.

코드로 살펴보겠습니다.

코드

const int MAX = 10;
int data[MAX] = { 5, 14, 2, 7, 11, 3, 6, 10, 9, 18, };
int result[20];

int main(int argc, char**argv) {
  for(int i = 0; i < MAX; i++) {
    int index = data[i];
    result[index]++;
  }

  for(int i = 0; i <= 20; i++) {
    if(result[i] > 0) {
      printf("%d ", i);
    }
  }
  printf("\n");

  return 0;
}


주의점

성능이 O(N)이긴 하지만, 항상 Quick Sort나 Merger Sort 보다 더 좋은 성능이 나오지는 않습니다. 예를 들어, N = 2, 데이터는 {1, 999999} 라는 데이터가 있으면 카운팅 정렬을 쓸 경우 훨씬 더 안 좋은 성능이 나올 것입니다.

하지만, 일반적인 실 생활에서나 알고리즘 대회 등에서는 카운팅 정렬을 사용할 기회가 많이 있습니다. { 1, 3, 2, 3, 2, 1, 3, 2, 1, } 와 같이 최대값이 적은 경우는 아주 효과가 큽니다. 그리고 꼭 자연수일 필요는 없습니다. 음수일 경우 모든 값에 특정 값을 더해서 양수로 만들어서 적용할 수도 있고, 소수일 경우엔 특정 값을 곱해서 정수처럼 만들어서 사용할 수도 있습니다. 구현도 쉽기 때문에 정렬이 필요할 때 제일 먼저 카운팅 정렬을 떠올려보고 적용이 가능한지 판단해보는 것도 좋을 것 같습니다.

터미널 apt-get에서 Tab 키 눌렀을 때 패키지 이름 자동 완성 안 될 때

|

터미널에서 Tab을 누르면 기본적으로 그 알파벳으로 시작하는 폴더, 파일, 패키지 등을 자동으로 완성해주는 역할을 합니다. 만약, 후보군이 여러 개 있을 때는 그 목록을 보여줍니다.

그런데, 가끔씩 apt-get 명령어를 이용해서 패키지 인스톨을 할 때, Tab을 눌러도 자동 완성이 되지 않는 경우가 있습니다.

이런 경우에는 터미널에서

source /etc/bash_completion

를 입력하면 그 다음부터 자동 완성 기능이 활성화됩니다.