티스토리 뷰
분할정복
알고리즘 유형중에 분할정복 유형이 있다.
문제를 부분 문제로 나누어 각 부분 문제를 해결하고, 부분 문제들의 결과를 합쳐 기본 문제를 해결하는 유형이다.
정렬 알고리즘에서 합병정렬과 퀵정렬이 이에 속하는데, 합병 정렬에 대해 알아보고자 한다.
합병정렬
크게 세 부분으로 생각하면 된다.
1. 나눈다.
2. 정복한다.
3. 합친다.
1. 나눈다.
리스트를 절반씩 나눈다. 이를 재귀적으로 실행하기 때문에 계속해서 절반씩 나누어진다.
그림에서 1~3번째 줄 과정이다.
2. 정복한다.
1.나눈다 와 같은 함수라고 보면 된다. 나누다가 길이가 1이 된다는 것은 더 이상 나눌 수 없을 뿐더러 '정렬'된 상태라는 것이다.
그러므로 이 때 결과를 return 한다.
그림에서 4번째, 가운데 과정이다.
3. 합친다.
1과 2를 마치고 나면 머지 함수로 정렬된 리스트를 만드는 과정이다. 리스트 두 개를 파라미터로 사용하여 반복문을 돌면서 두 리스트를 비교, 정렬하여 최종 정렬 리스트를 반환한다.
그림에서 5~7번째 줄 과정이다.
1, 2번 과정은 merge_sort(my_list) 함수로, 3번 과정은 merge(list1, list2) 함수로 구현한다.
먼저 merge_sort 과정을 보자.
def merge_sort(my_list):
# base case - 길이가 0 또는 1이라면 정렬된것이므로 리턴
if len(my_list) < 2:
return my_list
# recursive case - divide and conquer
# my_list를 반씩 나눈다(divide)
left_half = my_list[:len(my_list)//2]
right_half = my_list[len(my_list)//2:]
# 양쪽을 각각 정렬하기 위해 merge_sort 함수를 재귀적으로 호출하여 부분 문제 해결(conquer)하고,
# merge 함수로 정렬된 두 리스트를 합쳐(combine)준다
return merge(merge_sort(left_half), merge_sort(right_half))
# 테스트
print(merge_sort([1, 3, 5, 7, 9, 11, 13, 11]))
처음 리스트가 들어오면 계속해서 절반씩 나눈다. 그리고 리턴문으로 머지소트 함수가 재귀로 실행되므로 다시 절반씩 나누고....
과정을 반복하다가 길이가 1이 되면 반환한다. 이 때 merge 함수가 실행되는데 인자로 각각 길이가 1인 좌측, 우측 요소가 생길 것이다.
그러면 merge 함수를 보자.
# 정렬된 리스트를 돌려주는 함수
def merge(list1, list2):
i,j = 0, 0
merged_list = []
while i < len(list1) and j < len(list2):
if list1[i] > list2[j]:
merged_list.append(list2[j])
j += 1
else:
merged_list.append(list1[i])
i += 1
# (list1 끝나고) list2에 남은 항목이 있으면 정렬 리스트에 추가
if i == len(list1):
merged_list += list2[j:]
# (list2 끝나고) list1에 남은 항목이 있으면 정렬 리스트에 추가
if j == len(list2):
merged_list += list1[i:]
return merged_list
merge 함수는 길이가 1인 두 리스트를 받을 것이다.
두 리스트의 인덱스를 각각 돌면서 merged_list (정렬된 리스트)를 만들어나갈 것이다.
이 때 어느 한쪽이 먼저 끝나면 다른 한 쪽을 merged_list에 추가해준다.
다시 merge_sort 함수로 돌아와보자.
정렬된 리스트가 리턴되고, 다시 merge(merge_sort(left_half), merge_sort(right_half) , merge함수가 실행된다.
나눴던 횟수만큼 다시 merge 가 이루어진다.
최종적으로 하나의 merges_list가 반환된다.
시간 복잡도
병합 정렬의 시간 복잡도는 최악의 경우 O(N * logN) 이다.
일반적으로는 퀵 정렬보다 느리지만, 퀵 정렬의 경우 최악 O(N ^ 2)기 때문에 병합 정렬이 보다 안정적이다.
그러면 그림을 보며 시간 복잡도를 증명해보자.
정렬은 합치는 순간 실행되는데, 합치는 단계는 log_2(N) 이다.
즉, [38, 27, 43, 3, 9, 82, 10] 리스트가 있다면 길이가 1인 개별 배열에서 다시 머지해갈 때,
2개 x 4묶음 > 4개 x 2묶음 > 8개 x 1묶음 으로 총 깊이(merging time) 3 이다.
즉, 데이터의 갯수가 N 개일 때 log_2(N) 이다.
그리고 매 단계에서 정렬 자체에 필요한 수행시간은 N이다.
N이 8이라면 1번씩 8번, 2번씩 4번, 4번씩 2번 비교하기 때문이다.
따라서 너비가 N, 높이가 logN이라고 생각하면 시간 복잡도는 O(N * log_2(N)) 이다.
평균, 최악, 최고 모두 O(N * log_2(N)) 이다.
왜냐하면 머지소트는 항상 배열을 둘로 나누고 선형 시간만큼 그 둘을 합치기 때문이다.
이번엔 수식으로 증명해보자.
재귀 알고리즘이라는 점에 주목하자.
T(n) = 2T(n/2) + ɵ(n)
MergeSort(arr[], l, r)
If r > l
1. Find the middle point to divide the array into two halves:
middle m = (l+r)/2
2. Call mergeSort for first half:
Call mergeSort(arr, l, m)
3. Call mergeSort for second half:
Call mergeSort(arr, m+1, r)
4. Merge the two halves sorted in step 2 and 3:
Call merge(arr, l, m, r)
위는 합병 정렬의 슈도 코드이다.
1. 가운데를 찾아 두 배열로 나눈다. middle m = (l+r)/2
2. 앞 절반에 대하여 mergeSort 함수를 호출한다. Call mergeSort(arr, l, m)
3. 뒤 절반에 대하여 mergeSort 함수를 호출한다. Call mergeSort(arr, m+1, r)
4. 2와 3에서 정렬된 두개의 반으로 나눠진 리스트들을 합친다. : Call merge(arr, l, m, r)
두 가지 방식으로 재귀식의 시간복잡도를 증명할 수 있다.
'Algorithm' 카테고리의 다른 글
[SWEA][stack] 4866_괄호검사 (211019) (0) | 2021.10.19 |
---|---|
[SWEA][stack] 4875_미로 (211018) (0) | 2021.10.18 |
파이썬으로 순열과 조합 구현하기 (+중복순열, 중복조합) (3) | 2021.10.16 |
최단 경로와 다익스트라 알고리즘(Dijkstra Shortcut Path) (0) | 2021.10.16 |
서로소 집합과 MST 최소신장트리 / 프림 & 크루스칼 알고리즘 (0) | 2021.10.15 |
- Total
- Today
- Yesterday
- 상업용무료폰트
- 싸피6기
- 클린코드
- ssafy합격후기
- 무료폰트추천
- 맥과윈도우로깃허브
- ssafy후기
- 개발자도서추천
- ssafy결과
- 코딩도서
- 개발언어추천
- 개발자커리
- 폰트추천
- 임대차3법
- 개발자
- 한글무료폰트추천
- 싸피
- ssafy6기
- 깃허브계정
- intj여자
- 클린코더
- 개발자로드맵
- 개발자책추천
- 디즈니얼굴
- 개발언어순위
- 브왈라
- 폰트
- 개발도서추천
- 깃허브계정2개
- SSAFY
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |