알고리즘

정렬

Themion 2022. 1. 22. 12:07

정렬은 컴퓨터 과학에서 가장 기초적이면서 중요한 내용 중 하나이다. 당장 핸드폰 연락처나 컴퓨터 파일 시스템도 이름 순으로 정렬되어 있고, 이분 탐색이나 투 포인터는 정렬된 목록에서만 동작하는 등 정말 많은 곳에서 사용한다. 기초적이고 중요한 만큼 정렬하는 방법도 여러 가지가 있는데, 가장 유명하고 또 많이 쓰이는 방법은 버블 정렬, 힙 정렬, 퀵 정렬, 기수 정렬 이렇게 네 가지이다.

버블 정렬은 가장 간단하고 가장 느린 방법으로, 배열에서 가장 작은 원소를 찾아 첫 번째 값과 교환하고, 두 번째로 작은 원소를 찾아 두 번째 값과 교환하고, 세 번째로 작은 원소를 찾아 세 번째 값과 교환하고, 이 과정을 배열의 모든 순서에 대해 실행한다. 시간 복잡도는 n번 비교하여 한 번 교환하는 작업을 n번 진행하므로 O(n^2)가 된다.

for (int i = 0; i < len; i++)
    for (int j = i + 1; j < len; j++)
        if (arr[i] > arr[j]) swap(arr[i], arr[j]);

힙 정렬은 배열을 순차적으로 정렬하는 것이 아니라, 가장 작은 값이 항상 첫 번째로 오는 자료 구조인 힙을 만들어 값을 순차적으로 제거할 수 있게 하는 것을 말한다. 주로 값이 자주 추가되는 경우에 많이 사용되고, 배열의 길이 n에 대해 힙의 높이는 log2(n)이므로 시간 복잡도는 O(n * log2(n))이다. 직접 구현해서 사용할 수도 있지만, STL에선 queue 헤더의 priority_queue가 힙을 이용해 구현되었으므로 직접 구현할 필요 없이 사용할 수 있다.

int get_child(int parent) {
    int left = parent * 2, right = parent * 2 + 1;

    if (right > MAX || (!heap[left] && !heap[right])) return 0;

    if (!heap[left]) return right;
    if (!heap[right]) return left;
    return heap[left] < heap[right] ? left : right;
}

int pop() {
    int idx = 1, cmp, ret = heap[idx];
    
    heap[idx] = heap[heap[0]];
    heap[heap[0]] = 0;

    while ((cmp = get_child(idx)) && heap[idx] > heap[cmp]) 
        idx = swap(idx, cmp);

    heap[0] -= (heap[0] > 0);

    return ret;
}

void push(int item) {
    int idx = ++heap[0];
    heap[idx] = item;
    while (idx > 1 && heap[idx] < heap[idx / 2]) idx = swap(idx, idx / 2);
}

퀵 정렬은 가장 자주 사용되는 정렬 방법 중 하나로, 우선 배열에서 성분 하나를 골라 해당 성분보다 작은 값과 큰 값으로 나눈다. 이후 작은 값들은 고른 성분의 왼쪽에, 큰 값들은 고른 성분의 오른쪽에 둔 뒤, 작은 값과 큰 값에 대해 같은 작업을 반복한다. 이 과정을 각 작은 값과 큰 값의 개수가 1 이하일 때까지 반복하면 모든 값을 정렬할 수 있다. 퀵 정렬 역시 직접 구현해서 사용할 수도 있지만, STL에선 algorithm 헤더의 sort 함수가 퀵 정렬을 이용해 구현되었으므로 직접 구현할 필요 없이 사용할 수 있다.

void sort(int* start, int* end) {
    int pivot = *(end - 1), *l = start, *r = start;
    if (end - start <= 1)
        return;

    while ((r + 1) < end) {
        while ((l + 1) < end && *l < pivot) l++;
        for (r = l; (r + 1) < end && *r > pivot; r++);
        swap(l, r);
    }
    sort(start, l);
    sort(l + 1, end);
}

기수 정렬은 위의 세 정렬과 조금 다른데, 예를 들어 자연수를 정렬한다고 하면 우선 자연수의 1의 자리가 같은 수를 순서대로 모은 뒤, 다시 10의 자리가 같은 수를 순서대로 모으고, 100의 자리가 같은 수를 순서대로 모으고, 이 방법을 계속 반복해 모든 자리에 대해 비교한 다음 다시 순서대로 나열해 정렬하는 것을 말한다. 알고리즘 문제에서 기수정렬 자체를 사용할 일은 거의 없고, 대신 기수 정렬을 조금 변형해 각 수가 나온 횟수를 세어 작은 수부터 나온 횟수만큼 출력하는 식으로 사용하기도 한다.

void sort(int* start, int* end) {
    int arr[MAX_NUMBER - MIN_NUMBER + 1] = { 0, }, idx = 0;
    for (auto it = start; it != end; it++) arr[*it - MIN_NUMBER]++;
    for (auto it = start; it != end; it++) {
        while (!arr[idx]) idx++;
        *it = idx + MIN_NUMBER;
        arr[idx]--;
    }
}

'알고리즘' 카테고리의 다른 글

그리디 알고리즘  (0) 2022.01.22
브루트포스와 백트래킹  (0) 2022.01.22
에라토스테네스의 체  (0) 2022.01.22
그래프와 탐색  (0) 2022.01.22
다이나믹 프로그래밍  (0) 2022.01.22