알고리즘

정렬 - 병합정렬

na_o 2021. 8. 28. 01:23
728x90

*** 면접에서 직접 구현해보라고 하는 구현 방법!! ***

 

 

[병합 정렬 - merge]

배열의 앞부분과 뒷부분의 두 그룹으로 나누어 각각 정렬한 후 병합하는 작업을 반복하는 알고리즘

[1, 2, 3, 5]  # 정렬된 배열 A
[4, 6, 7, 8]  # 정렬된 배열 B
[] # 두 집합을 합칠 빈 배열 C


         ↓
1단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        1과 4를 비교
        1 < 4 이므로 1을 C 에 넣음
     C:[1]

            ↓
2단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        2와 4를 비교
        2 < 4 이므로 2를 C 에 넣음
     C:[1, 2]


               ↓
3단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        3과 4를 비교
        3 < 4 이므로 3을 C 에 넣음
     C:[1, 2, 3]

                  ↓
3단계 : [1, 2, 3, 5] 
        ↓
       [4, 6, 7, 8] 
        5와 4를 비교
        5 > 4 이므로 4을 C 에 넣음
     C:[1, 2, 3, 4]

                  ↓
3단계 : [1, 2, 3, 5] 
           ↓
       [4, 6, 7, 8] 
        5와 6을 비교
        5 < 6 이므로 5을 C 에 넣음
     C:[1, 2, 3, 4, 5]

A 의 모든 원소는 끝

B에서 안 들어간 원소인
       [6, 7, 8] 은 하나씩 C 에 추가
     C:[1, 2, 3, 4, 5, 6, 7, 8]

 


 

풀이)

두 개의 배열을 합치기 위해서 각 배열의 값들을 하나씩 비교

더 작은 값을 새로운 배열에 하나씩 추가

한 배열이 끝나면 남은 배열의 모든 값을 새로운 배열에 추가

 

result 라는 새로운 배열을 만든 다음에, while 문을 이용해서 비교 이후에 값을 추가

array_a = [1, 2, 3, 5]
array_b = [4, 6, 7, 8]


def merge(array1, array2):
    result = []
    array1_index = 0
    array2_index = 0
    while array1_index < len(array1) and array2_index < len(array2):
        if array1[array1_index] < array2[array2_index]:
            result.append(array1[array1_index])
            array1_index += 1
        else:
            result.append(array2[array2_index])
            array2_index += 1

    if array1_index == len(array1):     # array2가 남아있다
        while array2_index < len(array2):
            result.append(array2[array2_index])
            array2_index += 1

    if array2_index == len(array2):     # array1이 남아있다
        while array1_index < len(array1):
            result.append(array1[array1_index])
            array1_index += 1

    return result


print(merge(array_a, array_b))  # [1, 2, 3, 4, 5, 6, 7, 8]

 

 

 

나의 풀이)

array_a = [1, 2, 3, 4, 5]
array_b = [4, 6, 7, 8]


def merge(array1, array2):
    merge_array = []
    array1_index = 0
    array2_index = 0
    array1_len = len(array1)
    array2_len = len(array2)
    # array1과 array2 둘 중 하나가 끝 인덱스를 넘어갈 때까지 반복
    while array1_index <= array1_len-1 and array2_index <= array2_len-1:
        if array1[array1_index] < array2[array2_index]:
            merge_array.append(array1[array1_index])
            array1_index += 1
        else:
            merge_array.append(array2[array2_index])
            array2_index += 1
    # array2가 merge_array에 append를 모두 했음에도 불구하고 array1의 값들이 남아있는 상황
    if array1_index > array1_len-1:
        while array2_index <= array2_len-1:
            merge_array.append(array2[array2_index])
            array2_index += 1
    # array1가 merge_array에 append를 모두 했음에도 불구하고 array2의 값들이 남아있는 상황
    if array2_index > array2_len-1:
        while array1_index <= array1_len-1:
            merge_array.append(array1[array1_index])
            array1_index += 1
    return merge_array


print(merge(array_a, array_b))  # [1, 2, 3, 4, 5, 6, 7, 8]

 

 


 

분할 정복의 개념을 적용하여 '병합정렬'을 구현할 수 있음!

분할 정복(Divide and Conquer):

        문제를 작은 2개의 문제로 분리하고 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결하는 전략

 

[5, 4] 라는 배열이 있다면 
이 배열을 [5] 와 [4] 를 가진 각각의 배열로 작은 2개의 문제로 분리해서 생각하는 것임
그러면 이 둘을 합치면서 정렬한다면 결국 전체의 정렬된 리스트가 될 수 있음

이 개념을 조금 더 확대해서 생각해보면

[5, 3, 2, 1, 6, 8, 7,  4] 이라는 배열을 
반으로 쪼개면
[5, 3, 2, 1] [6, 8, 7, 4] 이 됨
또 반으로 쪼개면
[5, 3] [2, 1] [6, 8]  [7, 4] 이 됨
또 반으로 쪼개면
[5] [3] [2] [1] [6] [8] [7] [4] 이 됨

이 배열들을 두개씩 병합을 하면
[5] [3] 을 병합하면 [3, 5] 로
[2] [1] 을 병합하면 [1, 2] 로
[6] [8] 을 병합하면 [6, 8] 로
[7] [4] 을 병합하면 [4, 7] 로

[3, 5] 과 [1, 2]을 병합하면 [1, 2, 3, 5] 로
[6, 8] 와 [4, 7]을 병합하면 [4, 6, 7, 8] 로


[1, 2, 3, 5] 과 [4, 6, 7, 8] 를 병합하면 [1, 2, 3, 4, 5, 6, 7, 8] 이 됨

 

위의 과정을 보고 재귀함수가 떠오름!!!

MergeSort(시작점, 끝점)

MergeSort(0, N) = Merge(MergeSort(0, N/2) + MergeSort(N/2, N))

0부터 N까지 정렬한 걸 보기 위해서는 0부터 N/2 까지 정렬한 것과 N/2부터 N까지 정렬한 걸 합치면 됨

 

문자열의 길이가 1보다 작거나 같을 때 하나밖에 없으니까 무조건 정렬되었다고 볼 수 있을테니 탈출

def merge_sort(array):
    if len(array) <= 1:
        return array
    mid = (0 + len(array)) // 2
    left_array = merge_sort(array[:mid])   # 왼쪽 부분을 정렬하고
    right_array = merge_sort(array[mid:])  # 오른쪽 부분을 정렬한 다음
    merge(left_array, right_array)         # 합치면서 정렬

 


 

[병합 정렬 - merge sort]

잘 모르겠다면 print문으로 출력해보면서 과정을 확인하기

array = [5, 3, 2, 1, 6, 8, 7, 4]


def merge_sort(array):
    if len(array) <= 1:
        return array
    mid = len(array) // 2
    left_array = merge_sort(array[:mid])
    right_array = merge_sort(array[mid:])
    # print(array)
    # print('left_arary', left_array)
    # print('right_arary', right_array)
    return merge(left_array, right_array)


def merge(array1, array2):
    result = []
    array1_index = 0
    array2_index = 0
    while array1_index < len(array1) and array2_index < len(array2):
        if array1[array1_index] < array2[array2_index]:
            result.append(array1[array1_index])
            array1_index += 1
        else:
            result.append(array2[array2_index])
            array2_index += 1

    if array1_index == len(array1):
        while array2_index < len(array2):
            result.append(array2[array2_index])
            array2_index += 1

    if array2_index == len(array2):
        while array1_index < len(array1):
            result.append(array1[array1_index])
            array1_index += 1

    return result


print(merge_sort(array))  # [1, 2, 3, 4, 5, 6, 7, 8]

 

[출력 결과]

[5, 3, 2, 1, 6, 8, 7, 4]     맨 처음 array 이고
left_arary [5, 3, 2, 1]      이를 반으로 자른 left_array
right_arary [6, 8, 7, 4]     반으로 자른 나머지가 right_array

[5, 3, 2, 1]                 그 다음 merge_sort 함수에는 left_array 가 array 가 됨
left_arary [5, 3]            그리고 그 array를 반으로 자르고
right_arary [2, 1]           나머지를 또 right_array 에 넣은 뒤 반복

[5, 3]
left_arary [5]
right_arary [3]

[2, 1]
left_arary [2]
right_arary [1]

[6, 8, 7, 4]
left_arary [6, 8]
right_arary [7, 4]

[6, 8]
left_arary [6]
right_arary [8]

[7, 4]
left_arary [7]
right_arary [4]

 


[시간복잡도]

 

merge함수에서 while 문이 len(array1) 과 len(array2)의 길이 만큼 반복하고 있음

최대 len(array1) + len(array2) 만큼의 연산량이 필요한데

이의 최댓값은 O(N) !

array1과 array2 는 결국 array 를 반으로 잘라서 넣은 길이이기 때문

그러면 merge 함수의 시간 복잡도는 O(N)

 

merge_sort 함수는 대입 연산과 비교 연산 몇 번밖에 나오지 않으므로

상수의 시간 복잡도를 가짐

그렇지만 병합 정렬의 총 시간복잡도는 O(N)이라고 말하면 안됨!

 

1단계 
[1, 2, 3, 4, 5, 6, 7, 8] 을 정렬하기 위해          
[1, 2, 3, 5] 과 [4, 6, 7, 8] 을 병합  

즉, 크기 N 인 배열을 정렬하기 위해
크기 N/2 인 부분 배열 2개를 비교
그러면 N/2 * 2 = N 번 비교하게 됨

2단계
[1, 2, 3, 5] 과 [4, 6, 7, 8] 을 정렬하기 위해 
[3, 5] [1, 2] 과 [6, 8] [4,7] 을 병합

크기 N/2 인 부분 배열 2개를 정렬하기 위해
크기 N/4 인 부분 배열 4개를 비교
그러면 N/4 * 4 = N 번 비교하게 됨

3단계
[3, 5] [1, 2] 과 [6, 8] [4,7] 을 정렬하기 위해   
[5] [3] [2] [1] [6] [8] [7] [4] 를 병합

크기 N/4 인 부분 배열 4개를 정렬하기 위해
크기 N/8 인 부분 배열 8개를 비교
그러면 N/8 * 2 = N 번 비교하게 됨

즉 모든 단계에서 N 만큼 비교
그러면 총 몇 단계인지만 알면 시간 복잡도를 구할 수 있음

크기가 N  → N/2 → N/2^2 → N/2^3 → .... → 1

이 되는 순간이 있을것임
N/(2^K) = 1
k = log_2N

log_2N  번 반복하게 되면 1이 됨
이걸 수식으로 나타내면 
N만큼의 연산을 logN 번 반복한다고 해서 
log_2N * O(N) = O(Nlog_2N)

시간 복잡도는 총 O(Nlog_2N) -> O(NlogN) 이 됨!

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

스택  (0) 2021.08.29
정렬 - 삽입정렬  (0) 2021.08.27
정렬 - 선택정렬  (0) 2021.08.27
정렬 - 버블정렬  (0) 2021.08.26
더하거나 빼거나  (0) 2021.08.26