Merge Sort

Problem Solving with Algorithms and Data Structures using Python 을 공부하고 정리한 내용입니다.

sorting 알고리즘의 성능을 높이기 위해 Dived and Conquer 전략을 사용할 수 있습니다.

Merge Sort 는 리스트를 재귀적으로 두개로 나누어 정렬 후 정렬된 부분들을 다시 합치는 방식입니다.
base case는 merge 를 수행하는 부분이 되겠고, 그 조건은 리스트의 값이 하나 이하일 경우 입니다.

그림으로 예를 들어보겠습니다.

위의 그림은 Merge Sort 를 위해 리스트를 분할하는 과정을 나타냅니다.
리스트의 원소가 하나 이하가 될 때 까지 계속 분할하는 것을 볼 수 있습니다.

이렇게 분할된 리스트는

위와같이 다시 합치는 과정을 거치게 되는데, 이 때 합치는 과정은 정렬된 부분에서 가장 작은 값을 원래 리스트의 맨 앞으로 가져오는 형식으로 진행됩니다.

구현하기

  • 길이가 1 이하가 될 때 까지 입력리스트를 반으로 나누기 (Python 의 slicing 방법을 사용)
  • 반으로 나눈 리스트에 대해 각각 mergerSort 하기
  • 입력리스트, 왼쪽리스트, 오른쪽리스트 의 값을 비교하며 가장 작은 값부터 앞쪽에 넣어주기
def mergeSort(alist):
    print('Splitting {}'.format(alist))
    if len(alist) > 1:
        mid = len(alist) // 2
        leftList = alist[:mid]
        rightList = alist[mid:]
        
        mergeSort(leftList)
        mergeSort(rightList)
        
        leftIdx = 0
        rightIdx = 0
        originIdx = 0
        
        while leftIdx < len(leftList) and rightIdx < len(rightList):
            if leftList[leftIdx] < rightList[rightIdx]:
                alist[originIdx] = leftList[leftIdx]
                leftIdx = leftIdx + 1
            else:
                alist[originIdx] = rightList[rightIdx]
                rightIdx = rightIdx + 1
            originIdx = originIdx + 1
            
        while leftIdx < len(leftList):
            alist[originIdx] = leftList[leftIdx]
            leftIdx = leftIdx + 1
            originIdx = originIdx + 1
            
        while rightIdx < len(rightList):
            alist[originIdx] = rightList[rightIdx]
            rightIdx = rightIdx + 1
            originIdx = originIdx + 1
            
        print('Mergeing {}'.format(alist))

실행결과를 보면, 재귀적으로 구현되었기 때문에 왼쪽리스트부터 base case 까지 나눈 후 merge 하고
그 다음에 오른쪽리스트를 merge 하고 있는것을 확인할 수 있습니다.

mergeSort([54,26,93,17,77,31,44,55,20])
Splitting [54, 26, 93, 17, 77, 31, 44, 55, 20]
Splitting [54, 26, 93, 17]
Splitting [54, 26]
Splitting [54]
Splitting [26]
Mergeing [26, 54]
Splitting [93, 17]
Splitting [93]
Splitting [17]
Mergeing [17, 93]
Mergeing [17, 26, 54, 93]
Splitting [77, 31, 44, 55, 20]
Splitting [77, 31]
Splitting [77]
Splitting [31]
Mergeing [31, 77]
Splitting [44, 55, 20]
Splitting [44]
Splitting [55, 20]
Splitting [55]
Splitting [20]
Mergeing [20, 55]
Mergeing [20, 44, 55]
Mergeing [20, 31, 44, 55, 77]
Mergeing [17, 20, 26, 31, 44, 54, 55, 77, 93]

분석하기

Merge Sort 는 크게 두 가지의 과정으로 나뉘므로, 각 과정에 대해 생각해 볼 필요가 있습니다.

먼저 Split 의 과정을 생각해보면 Binary Search 와 같은 과정임을 알 수 있습니다.
n개의 값을 반으로 나눠가는 과정을 생각해 보면,
첫번째 과정을 거쳤을 때에는 $\frac {n}{2}$, 두번째는 $\frac {n}{4}$ , … , $k$ 번째에는 $\frac {n}{2^k}$ 개의 값이 남는 것을 알 수 있습니다.
우리는 $\frac {n}{2^k}$ (맨 마지막으로 남은 원소의 수) 가 1인 경우까지 split 하는 것이기 때문에, $\frac {n}{2^k} = 1$ 인 k 를 계산하면 됩니다.
$k=\log n$ 이므로 반으로 나눠가는 과정의 실행 시간은 $O(\log n)$ 만큼이 걸리는 것을 알 수 있습니다.

다음으로 Merge 의 과정은 각 값당 최대 n 번의 비교를 거쳐야 하기 때문에 $n$ 만큼의 시간이 걸립니다.

그래서 Merge Sort 알고리즘은 $O(n\log n)$ 알고리즘 입니다.

추가중

그런데 slicing 방법의 실행시간은 무시해도 되는걸까 하는 의문이 생겼습니다.
검색결과 slicing 은 $k$ 개의 slicing 을 할 때에 $O(k)$ 만큼의 실행시간이 필요하다고 합니다.

Merge Sort 의 이론적인 분석시간인 $O(n\log n)$ 을 맞춰주기 위해서는 이 slicing 방법을 제거해야 합니다.

hyunkyung's profile image

hyunkyung

2018-11-29 10:30

Read more posts by this author