Segment Tree

|

세그먼트 트리(Segment Tree)는 구간별 최대값, 최소값 등을 빠르게 구할 수 있게 해주는 트리 형태의 데이터 구조입니다.


Segment Tree 구조

Segment Tree는 완전 이진 트리이며, 부모 노드는 각 자식 노드들의 범위에 대한 정보를 갖고 있습니다. 각 노드간의 관계는 아래와 같습니다.

SegmentTree

실제 데이터는 맨 아래의 Leaf 노드들이 가지게 되며, 부모 노드들은 각각의 자식 노드들의 구간내의 정보를 갖게 됩니다. 예를 들어, 구간 내 최소값을 관리하는 Segment Tree를 알아보도록 하겠습니다.


구간별 최소값을 관리하는 Segment Tree

SegmentTree

실제 데이터는 대략 이런 식으로 저장이 됩니다. (최하위 노드에만 값이 저장됩니다.) 이 때, 부모 노드들에 들어갈 값들을 살펴보도록 하겠습니다.


SegmentTree

이런 식으로 각 부모 노드들은 해당 구간에 속하는 자식 노드들의 최소값을 저장하게 되며,


SegmentTree

차곡차곡 각 부모 노드들의 데이터가 채워지게 됩니다.


구간별 최소값 조회

이제, Segment Tree를 이용한 구간별 최소값 조회를 한 번 해보도록 하겠습니다. 예를 들어, 1번째 노드부터 7번째 노드까지의 최소값을 구해보겠습니다.

SegmentTree 원래는 1번째 노드부터 7번째 노드까지 차례대로 값을 확인해야 했지만, Segment Tree에서는 위 그림과 같이 3개의 노드만 검색을 하면 최소값을 얻을 수 있습니다. 데이터 사이즈가 커지면 더욱 속도 차이가 많이 나게 될 것입니다.


데이터 갱신

데이터 갱신을 할 경우 Segment Tree는 해당 범위를 포함하고 있는 모든 부모 노드들의 값을 다시 업데이트해줘야 합니다.

SegmentTree

만약 4번째 노드의 값이 변경될 경우, 이런 식으로 4번째 노드를 포함하고 있는 모든 부모 노드들의 값을 업데이트 해줘야 합니다.


SegmentTree


코드

Segment Tree 정의

Segment Tree의 구조를 보면 실제 데이터보다 약 2배만큼의 저장 공간을 더 사용하고 있습니다. (실제 데이터 공간과 그 범위를 관리하는 부모 노드들의 공간을 합치면 약 2배가 됩니다.)

따라서 아래 코드와 같이 실제 데이터보다 2배 정도의 크기를 선언해서 사용하면 되고, 초기값은 무한대로 설정했습니다.


static const int MAX_TREE_SIZE = 100000;
static const int INFINITE = 9999999;
int data[] = { 0, 6, 3, 7, 5, 2, 4, 8, 1, };
int N = 8;
int segment_tree[2 * MAX_TREE_SIZE];

int getMin(int a, int b) {
    if (a <= b) return a;
    return b;
}

void initialize() {
    int size = 2 * N - 1;
    for (int i = 1; i <= size; i++) {
        segment_tree[i] = INFINITE;
    }
}

void debug() {
    int size = 2 * N - 1;
    int pow = 2;
    for (int i = 1; i <= size; i++) {
        printf("%d ", segment_tree[i]);
        if (i == (pow - 1)) {
            printf("\n");
            pow = pow * 2;
        }
    }

    printf("\n");
}


데이터 갱신

각 노드들에 인덱스를 매겨보면,

SegmentTree

와 같이 인덱스를 붙일 수 있습니다. 부모 노드와 자식 노드간의 관계는 아래 그림과 같습니다.

SegmentTree

즉, 데이터 갱신은 다음과 같은 코드를 통해서 할 수 있습니다.

void update(int pos, int value) {
    pos = pos + (N -1);
    segment_tree[pos] = value;

    while (true) {
        pos = pos / 2;
        if (pos == 0) break;
        segment_tree[pos] =
            getMin(segment_tree[2 * pos], segment_tree[2 * pos + 1]);
    }
}


데이터 조회

데이터 조회 부분이 좀 복잡한데, 다음과 같은 재귀 함수를 이용해서 조회를 할 수 있습니다.

// left, right : 구하고자 하는 값의 범위
// segment_left, segment_right : 현재 Segment의 구간
// node_id : 현재 node의 index
int query(int left, int right, int segment_left,
    int segment_right, int node_id) {
    // 전혀 관련이 없으면 INFINITE 리턴
    if ((segment_right < left) || (segment_left > right))
        return INFINITE;

    // 만약 현재 구간이 구하고자 하는 값의 범위 안에 있으면 SegmentTree값 리턴
    if ((segment_left >= left) && (segment_right <= right))
        return segment_tree[node_id];

    // 현재 Segment의 중간점
    int mid = (segment_left + segment_right) / 2;

    int left_value = query(left, right,
        segment_left, mid, 2 * node_id);
    int right_value = query(left, right,
        mid + 1, segment_right, 2 * node_id + 1);

    return getMin(left_value, right_value);
}


전체 소스 코드

#include <stdio.h>

static const int MAX_TREE_SIZE = 100000;
static const int INFINITE = 9999999;
int data[] = { 0, 6, 3, 7, 5, 2, 4, 8, 1, };
int N = 8;
int segment_tree[2 * MAX_TREE_SIZE];

int getMin(int a, int b) {
    if (a <= b) return a;
    return b;
}

void initialize() {
    int size = 2 * N - 1;
    for (int i = 1; i <= size; i++) {
        segment_tree[i] = INFINITE;
    }
}

void debug() {
    int size = 2 * N - 1;
    int pow = 2;
    for (int i = 1; i <= size; i++) {
        printf("%d ", segment_tree[i]);
        if (i == (pow - 1)) {
            printf("\n");
            pow = pow * 2;
        }
    }

    printf("\n");
}

void update(int pos, int value) {
    pos = pos + (N -1);
    segment_tree[pos] = value;

    while (true) {
        pos = pos / 2;
        if (pos == 0) break;
        segment_tree[pos] =
            getMin(segment_tree[2 * pos], segment_tree[2 * pos + 1]);
    }
}

// left, right : 구하고자 하는 값의 범위
// segment_left, segment_right : 현재 Segment의 구간
// node_id : 현재 node의 index
int query(int left, int right, int segment_left,
    int segment_right, int node_id) {
    // 전혀 관련이 없으면 INFINITE 리턴
    if ((segment_right < left) || (segment_left > right))
        return INFINITE;

    // 만약 현재 구간이 구하고자 하는 값의 범위 안에 있으면 SegmentTree값 리턴
    if ((segment_left >= left) && (segment_right <= right))
        return segment_tree[node_id];

    // 현재 Segment의 중간점
    int mid = (segment_left + segment_right) / 2;

    int left_value = query(left, right,
        segment_left, mid, 2 * node_id);
    int right_value = query(left, right,
        mid + 1, segment_right, 2 * node_id + 1);

    return getMin(left_value, right_value);
}

int main(int argc, char** argv) {
    initialize();

    for (int i = 1; i <= N; i++) {
        update(i, data[i]);
    }

    // Segment Tree 출력
    debug();

    // 최소값을 구하고자 하는 구간 입력
    int left = 2;
    int right = 8;
    int value = query(left, right, 1, N, 1);
    printf("Minimum between %d and %d : %d\n",
        left, right, value);

    return 0;
}

정적 웹페이지 vs 동적 웹페이지

|

정적인 웹페이지를 생성시켜주는 툴인 Jekyll를 소개하고자 합니다. 그러기에 앞서 먼저 정적 웹페이지에 대해서 먼저 소개하도록 하겠습니다.


정적 웹페이지 vs 동적 웹페이지

이름에서 눈치를 챌 수 있듯이 사용자의 인터랙션에 따라 웹페이지가 바뀌어가는 것을 동적 웹페이지, 그렇지 않고 항상 같은 내용을 보여주는 웹페이지를 정적 웹페이지라고 합니다. 일반적인 게시판 형태의 사이트들은 데이터베이스(DB, Database)를 활용한 동적 웹페이지 방식을 사용하고 있고, 정적 웹페이지 방식의 경우에는 DB를 사용하지 않습니다. 정적 웹페이지 방식은 글 자체가 전부 html 단위의 파일들로 이루어져 있습니다.

흔히 말하는 블로그 서비스들인 티스토리네이버 블로그는 동적 웹페이지입니다. 사용자가 글을 작성하거나 블로그 설정 등을 바꾸면 그 내용이 서버에 있는 DB에 저장이 되고 그 결과가 웹페이지에 반영이 되는 형태로 동작합니다. 동적 웹페이지는 블로그에 접속해서 웹 브라우저 상에서 새 글 작성도 할 수 있고, 통계나 다양한 위젯 등 화려한 기능들을 많이 제공해줍니다.

그에 반해 정적 웹페이지는 단순히 사이트 관리자가 미리 만들어놓은 웹페이지만 볼 수 있습니다. 관리도 힘들고 새 글을 쓰거나 수정할 때도 글을 다운로드 받아 편집하고 업로드까지 수동으로 해야 합니다.

하지만, 이렇게 단점이 많은데도 정적 웹페이지가 주목받는 이유는 다음과 같습니다.


정적 웹페이지의 장점

  • 동적인 요소가 없기 때문에 데이터베이스 등이 필요없고, 구축이 쉬움.
  • 단순 문서로만 이루어져 있어서 서버간 통신이 거의 없고 속도가 빠름.
  • 정적인 문서들로만 이루어져 있기 때문에 어떤 호스팅 서버에서도 동작 가능.
  • 일반 텍스트 에디터기에서 문서를 작성이 가능. 즉, 익숙한 에디터기를 사용할 수 있음.
  • 백업, 복원이 쉬움.

위 와 같은 장점이 있기 때문입니다.

블로그 같은 경우는 역동적인 인터랙션보다는 대부분이 정적인 글들로 이루어져 있는 경우가 많아서 정적 웹페이지만으로도 충분히 훌륭한 결과물을 보여줄 수 있습니다. 그래서 블로그와 정적 웹페이지는 상성이 아주 좋아서 최근에 정적 웹페이지 방식의 블로그가 많이 생겨나고 있습니다.


정적 웹페이지 플랫폼

이미 수백 개의 정적 웹페이지 플랫폼들이 존재합니다. 그 중에서 대표적인 플랫폼으로 Jekyll이나 Hugo 등이 있습니다. 여기에서 좀 더 많은 정적 웹페이지 플랫폼 리스트를 볼 수 있습니다. (평점으로 정렬해보면 Jekyll은 당당하게 1위를 차지하고 있습니다.)

이 블로그는 Jekyll로 만들어졌졌습니다. 잠깐 Hugo으로 블로그를 운영해 본 적도 있는데, 다시 Jekyll로 돌아왔습니다.

Jekyll과 Hugo는 서로 장단점이 있습니다.


Jekyll vs Hugo

Jekyll Hugo
Ruby 기반 Go 언어 기반
설치 과정이 복잡하다. 단순 실행 파일만 있으면 된다.
컴파일 속도가 느림. 컴파일 속도가 빠름.
사용자 수가 아주 많다. Jekyll 만큼은 아니지만 사용자 수가 점점 많아지고 있다.
많은 Plugin & 테마를 지원한다. Jekyll 만큼은 아니지만 점점 증가하고 있다.

대부분이 Hugo 쪽이 더 좋은 것처럼 썼지만, 제가 Jekyll을 쓰는 이유는 GitHub Pages에는 Jekyll이 더 최적화가 되어 있다는 점입니다. Hugo를 이용할 경우 사이트를 Hugo로 빌드를 한 다음 그 결과물을 서버에 업로드해야 하지만 Jekyll의 경우는 그냥 소스 그대로 서버에 올리면 GitHub Pages가 알아서 빌드를 해주기 때문에 별도의 배포 작업이 필요없어 관리가 더 수월합니다.

물론 반대로 말하면 Hugo는 결과물만 서버에 올리기 때문에 원본 소스는 비교적 안전하다고 볼 수 있고, Jekyll은 전체 소스를 그대로 올리기 떄문에 아무나 소스를 가져갈 수 있다는 단점도 있습니다. 하지만, GitHub Pages에서 블로그를 운영하기로 한 이상 소스 보호는 큰 의미가 없는 것 같아서 관리가 수월한 Jekyll로 다시 돌아오게 되었습니다.

그리고, Jekyll의 사용자 수가 훨씬 더 많기 때문에 자료 찾기가 수월하다는 점도 있습니다.

Jekyll의 빌드 속도가 느리다거나 Ruby 기반이라 설치가 복잡하고 무겁다는 단점도 로컬 PC에서 빌드를 직접 돌릴 떄나 해당되는 말이며, 저 같은 경우는 그냥 로컬 빌드없이 서버상에서 포스팅하는 글만 관리하다보니 큰 문제는 없는 것 같습니다. (하지만, 테마를 적용한다던지 블로그 전체의 레이아웃이나 메뉴 등을 바꾼다거나 할 때는 미리 로컬 PC에서 확인하는 작업이 필요하기 때문에 결국은 Jekyll을 로컬 PC에 설치해야 할 것입니다. 단순히 글만 작성할 때는 상관없습니다.)

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로 바꾸겠다는 뜻입니다.