병합 정렬(Marge Sort)은 분할 정복 알고리즘에 기반한 정렬 알고리즘 중 하나로, 안정적이고 효율적인 정렬 방식입니다.
이번 포스트에서는 앞에서 병합 정렬 코드에 대해 알아보고 뒤에서는 병합 정렬을 시각화한 글을 보고 병합 과정을 따라가 볼 것입니다.
일단 병합 정렬의 핵심 3가지를 알아보겠습니다.
- 분할(Divide)
- 배열(또는 리스트)을 절반씩 나눕니다.
- 나누는 과정을 재귀적으로 반복하여, 크기가 1인 배열로 쪼갭니다.
- 크기가 1인 배열은 이미 정렬된 상태라고 가정합니다.
- 정복(Conquer)
- 작은 두 배열을 비교하면서 정렬하여 병합(Marge)합니다.
- 병합 과정에서 두 배열을 한 번에 순회하며, 각 리스트의 첫 번째 요소를 비교하여 더 작은 값을 새로운 리스트에 추가하는 방식으로 진행합니다.
- 병합(Merge)
- 두 개의 정렬된 배열을 하나의 정렬된 배열로 병합(Marge)합니다.
- 이를 통해 마지막 단계에서 정렬된 배열을 얻습니다.
병합 정렬의 기본 구현을 알아보겠습니다.
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 예시 입력
unsorted_list = [3, 7, 1, 4, 1, 5]
sorted_list = merge_sort(unsorted_list)
print(sorted_list) #[1, 1, 3, 4, 5, 7]
코드를 분석해 보겠습니다.
- marge_sort() 함수
이 함수는 재귀적으로 배열을 나누고, 병합하여 정렬하는 병합 정렬의 핵심입니다.
- 기저 조건:
배열의 길이가 1 이하라면 이미 정렬된 상태로 간주하고 그대로 반환합니다.if len(arr) <= 1: return arr
- 배열 분할:
배열의 중간 인덱스 mid를 기준으로 두 부분으로 나눕니다. arr[:mid]는 왼쪽 절반, arr[mid:]는 오른쪽 절반을 의미합니다.mid = len(arr) // 2 left_half = merge_sort(arr[:mid]) right_half = merge_sort(arr[mid:])
이 과정을 재귀적으로 호출하여 단일 원소로 나눌 때까지 반복합니다. - 병합:
나뉜 두 부분을 marge() 함수로 병합하며 정렬합니다.return merge(left_half, right_half)
- 기저 조건:
- marge() 함수
이 함수는 두 개의 정렬된 리스트를 병합하여 하나의 정렬된 리스트를 반환합니다.
- 병합 과정:
두 리스트 left, right의 첫 번째 요소를 비교하며 더 작은 값을 sorted_array에 append 합니다.while i < len(left) and j < len(right): if left[i] < right[j]: sorted_array.append(left[i]) i += 1 else: sorted_array.append(right[j]) j += 1
비교 후에 해당 리스트의 인덱스를 증가시킵니다. - 남은 요소 처리:
한쪽 리스트의 요소가 모두 출력되었을 경우, 다른 쪽 리스트에 남아 있는 요소들을 결과 리스트에 추가합니다.sorted_array.extend(left[i:]) sorted_array.extend(right[j:])
- 병합 과정:
병합 정렬(Merge Sort)은 컴퓨터 과학에서 가장 중요한 정렬 알고리즘 중 하나로, 효율성과 안정성을 모두 갖춘 강력한 알고리즘입니다만, 재귀 호출과 병합 과정이 반복적으로 이루어지기 때문에 다소 어렵게 느껴질 수 있습니다.
이에 따라, 병합 정렬의 과정을 단계별로 상세히 설명하며, 각 단계에서 어떤 일이 일어나는지 번호 체계와 기호를 통해 시각적으로 표현하고자 했습니다. 이를 통해 독자들이 위의 코드를 보고 이해하기 어려웠다면, 글을 읽으면서 병합 정렬의 동작 원리를 쉽게 따라갈 수 있도록 돕고자 했습니다..
병합 정렬의 과정을 단계적으로 표현하기 위해 아래와 같은 기호와 들여 쓰기 규칙을 사용했습니다. 사실
- 번호 체계
- '깊이-활동 순서' 형식으로 번호를 매겼습니다.
- 첫 번째 숫자: 재귀 호출의 깊이를 나타냅니다.
- 예: 0-1은 깊이 0(최상위 호출)을 의미합니다.
- 깊이가 증가할수록 더 작은 리스트를 처리하는 단계입니다.
- 두 번째 숫자: 해당 깊이에서의 활동 순서를 나타냅니다.
- 예: 1-1은 깊이 1에서 첫 번째 활동(왼쪽 리스트 처리)을 의미합니다.
- 첫 번째 숫자: 재귀 호출의 깊이를 나타냅니다.
- '깊이-활동 순서' 형식으로 번호를 매겼습니다.
- 들여 쓰기
- 들여 쓰기는 재귀 호출의 깊이를 시각적으로 표현합니다.
- 들여 쓰기가 깊어질수록 더 작은 리스트를 처리하는 단계입니다.
- 예: 1-1은 0-1보다 한 단계 더 깊은 재귀 호출을 나타냅니다.
- 들여 쓰기는 재귀 호출의 깊이를 시각적으로 표현합니다.
- 주요 기호
- return [x]: 리스트가 길이가 1이 되어 반환되는 시점입니다.
- end: 현재 활동(재귀 호출 또는 병합)이 종료되는 시점입니다.
- Merging [A] and [B]: 두 리스트 [A]와 [B]가 병합되는 과정을 나타냅니다. marge() 함수 속입니다.
- result.append(x): 병합 과정에서 결과 리스트에 요소를 추가하는 단계입니다.
- result.extend(right[j:]): 병합 과정에서 한쪽 리스트의 남은 요소를 결과 리스트에 추가하는 단계입니다.
아래는 시각화한 병합 정렬 과정입니다.
0-1: merge_sort([3, 7, 1]) # 깊이 0, 전체 리스트 처리
1-1: left = merge_sort([3]) # 깊이 1, 왼쪽 리스트 처리
2-1: return [3] # 깊이 2, 리스트 길이가 1이므로 반환
2-1: end
1-1: left = [3]
1-1: right = merge_sort([7, 1]) # 깊이 1, 오른쪽 리스트 처리
2-2: left = merge_sort([7]) # 깊이 2, 왼쪽 리스트 처리
3-1: return [7] # 깊이 3, 리스트 길이가 1이므로 반환
3-1: end
2-2: left = [7]
2-2: right = merge_sort([1]) # 깊이 2, 오른쪽 리스트 처리
3-2: return [1] # 깊이 3, 리스트 길이가 1이므로 반환
3-2: end
2-2: right = [1]
2-2: return merge([7], [1]) #merge() 함수로 들어갑니다.
Merging [7] and [1] -> result: [1,7]
2-2: result = [1,7] #merge() 함수의 반환값입니다.
2-2: end
1-1: right = [1,7]
1-2: return merge([3], [1,7]) # 최종 병합 수행
Merging [3] and [1,7]:
result.append(1) -> result = [1]
i = 0, j = 1
result.append(3) -> result = [1,3]
i = 1, j = 1
반복문 종료
result.extend(right[j:]) -> result = [1,3,7] (right[j:] = [7])
marge([3], [1,7]) -> result: [1,3,7]
1-2: end
0-1: end
# 이 글의 내용 중 일부는 Perplexity AI의 도움을 받아 정보를 수집하고 정리하였으며, 추가적인 연구와 개인적인 견해를 더하였습니다.
'파이썬 관련 문법' 카테고리의 다른 글
파이썬의 요소 추가 메서드 비교 (1) | 2024.12.15 |
---|---|
단일 패스 탐색: 투 포인터 기법의 활용 (0) | 2024.12.12 |
파이썬의 양방향 큐, deque 사용법 (1) | 2024.12.11 |
Counter 클래스: 코딩 테스트를 위한 효율적인 요소 빈도 계산 도구 (1) | 2024.12.11 |
이진 탐색이란 무엇인가? (0) | 2024.12.10 |