[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

💻 CS/자료구조 & 알고리즘

[알고리즘] 합병 정렬 (분할정복 활용)

inu 2020. 2. 15. 16:43
반응형

특정 리스트를 정렬하는 방법에는 여러 가지가 있다.

그중에서도 대표적으로 분할 정복을 활용하여 리스트를 정렬하는 것이 합병 정렬, 퀵 정렬이다.

그 중 합병 정렬에 대해 알아보자. 합병 정렬은 병합 정렬이라고도 불린다.

정렬된 두 리스트의 합 정렬

먼저 알아야 할 것이 있다. 합병 정렬은 정렬된 임의의 두 리스트가 있을 때, 두 리스트를 합치면서 정렬하기 위해서는 두 리스트의 성분을 처음부터 하나씩 비교해면서 새로운 리스트에 넣어주다가 둘 중 어느 한 리스트의 성분 모두 없어지면, 나머지는 그냥 새로운 리스트의 뒤에 들어가면 된다.

ex. [1,2,3,5] [4,6,7,8]이 있을 때 이를 합치면서 정렬하려 한다. 처음부터 성분을 비교하면서 넣다 보면 어느 순간 새로운 리스트에 [1,2,3,4,5]까지 들어가고 두 리스트가 [] [6,7,8] 상태가 될 것이다. 이때 남은 리스트에 있는 6,7,8 성분은 그냥 새로운 리스트 [1,2,3,4,5] 뒤에 붙여 [1,2,3,4,5,6,7,8]를 만들어주면 된다.

이러한 정렬된 임의의 두 리스트를 합칠 때의 과정에서 분할 정복의 개념을 더해 착안해 낸 것이 합병 정렬이다.

합병 정렬의 과정

리스트를 먼저 나눈다.(divide) 그리고 각각의 문제를 해결해야 하는데 여기서는 분할 정복을 반복해서 활용해야 하므로 리스트의 길이가 1이 될 때까지 문제를 나눠준다.(conquer) 리스트의 길이가 1일 경우 각각의 나눈 리스트를 더한다.(combine) 리스트를 더할 때는 위에서 본 '정렬된 두 리스트의 합 정렬'과정으로 더한다. combine 되는 리스트는 그 길이가 1일지라도 최소한 정렬된 알고리즘이기 때문에 정상적으로 정렬될 수 있다.

cf. 분할 정복의 3단계는 divide, conquer, combine이라고 했다. 하지만 이 셋만큼이나 중요한 것이 또 하나 있는데, 바로 base-case의 설정이다. base-case를 제대로 설정해주지 않으면 재귀를 활용한 분할 정복이 제대로 이뤄지지 않을 것이다. 위에서는 리스트의 길이가 1이 되었을 때가 base-case이다.

합병 정렬 구현 (by. C++)

#include <stdio.h>
int number = 8;

int size;
int sorted[8]; // 정렬할 때 사용할 배열, 해당 배열은 반드시 전역 변수로 선언 
int count = 0;

void merge(int a[], int m, int middle, int n) {
    int i = m;
    int j = middle + 1;
    int k = m;
    // 작은 순서대로 배열에 삽입 
    while(i <= middle && j <= n) {
        if(a[i] <= a[j]) {
            sorted[k] = a[i];
            i++;
        } else {
            sorted[k] = a[j];
            j++;
        }
        k++;
    }
    // 남은 데이터 삽입 
    if(i > middle) {
        for(int t = j; t <= n; t++) {
            sorted[k] = a[t];
            k++;
        }
    } else {
        for(int t = i; t <= middle; t++) {
            sorted[k] = a[t];
            k++;
        }
    }
    // 정렬된 배열을 원래의 배열에 삽입 
    for(int t = m; t <= n; t++) {
        a[t] = sorted[t];
    }
}

void mergeSort(int a[], int m, int n) {
    // 이외의 경우는 크기가 1개인 경우 
    if(m < n) {
        int middle = (m + n) / 2;
        mergeSort(a, m, middle);
        mergeSort(a, middle + 1, n);
        merge(a, m, middle, n);
    }
}

int main(void) {
    int array[number] = {7, 6, 5, 8, 3, 5, 9, 1};
    mergeSort(array, 0, number - 1);
    for(int i = 0; i < number; i++) {
        printf("%d ", array[i]);
    }
} 

합병 정렬 구현 (by. python)

def combine(num_list, start_idx, mid_idx, end_idx):
    l_idx = start_idx # start_idx ~ mid_idx
    r_idx = mid_idx+1 # mid_idx+1 ~ end_idx

    sorted_list = []

    while l_idx <= mid_idx and r_idx <= end_idx:
        # if l_idx > mid_idx:
        #     # sorted_list.append(num_list[r_idx])
        #     # r_idx += 1
        #     sorted_list += num_list[r_idx:end_idx+1]
        #     r_idx = end_idx + 1
        # elif r_idx > end_idx:
        #     # sorted_list.append(num_list[l_idx])
        #     # l_idx += 1
        #     sorted_list += num_list[l_idx:mid_idx+1]
        #     l_idx = mid_idx + 1
        if num_list[l_idx] < num_list[r_idx]:
            sorted_list.append(num_list[l_idx])
            l_idx += 1
        else:
            sorted_list.append(num_list[r_idx])
            r_idx += 1

    sorted_list += num_list[r_idx:end_idx+1]
    sorted_list += num_list[l_idx:mid_idx+1]

    num_list[start_idx:end_idx+1] = sorted_list

def bottom_up_merge_sort(num_list):
    size = 1

    while size < len(num_list):
        start_idx = 0

        while start_idx < len(num_list):
            end_idx = min(start_idx + size * 2 - 1, len(num_list)-1)
            mid_idx = min(start_idx + size - 1, end_idx)

            combine(num_list, start_idx, mid_idx, end_idx)
            print(num_list)

            start_idx = end_idx + 1

        size *= 2

n = int(input())
num_list = []

for _ in range(n):
    num_list.append(int(input()))

bottom_up_merge_sort(num_list)

합병 정렬 과정 이미지

이미지 출처 : 위키백과 ( https://ko.wikipedia.org/wiki/%ED%95%A9%EB%B3%91_%EC%A0%95%EB%A0%AC )

반응형