카테고리 없음

[알고리즘] Merge Sort: 병합 정렬

뱌재데 2024. 6. 23. 21:28
728x90

 

 

병합 정렬 알고리즘 - 출처 위키피디아

 

병합 정렬(Merge Sort)은 분할 정복(divide and conquer) 방식을 사용하는 정렬 알고리즘입니다. 이 방식은 큰 문제를 작은 문제로 나눈 뒤, 각각 해결한 다음 그 해결된 작은 문제들을 다시 합쳐서 원래의 문제를 해결하는 방식입니다. 병합 정렬은 주로 리스트를 반으로 계속 나누고, 나누어진 각 부분을 정렬한 다음에 다시 합치는 과정을 반복합니다.

 

병합 정렬의 과정:

병합 정렬은 크게 두 가지 주요 단계로 나누어집니다: 분할(divide)과 정복(conquer) 단계입니다.

 

1. 분할 단계:

- 주어진 리스트를 반으로 계속 나눕니다. 각 부분이 충분히 작아질 때까지 나누는데, 보통 하나의 원소만 남을 때까지 나눕니다.

- 하나의 원소만 있는 리스트는 자연스럽게 정렬된 것으로 간주할 수 있습니다.

 

2. 정복 단계:

- 나누어진 리스트들을 다시 합치면서 정렬합니다. 이를 위해 두 개의 작은 리스트를 합치는 작업을 반복합니다.

- 두 리스트의 가장 앞 원소를 비교하여 더 작은 원소를 새 리스트에 추가합니다. 이 과정을 두 리스트의 모든 원소가 새 리스트로 이동할 때까지 반복합니다.

 

 

 

 
병합 정렬 과정 예시 이미지

 

 

1. 초기 배열: [38, 27, 43, 3, 9, 82, 10]

 

2. 첫 번째 분할:

- 왼쪽 부분 배열: [38, 27, 43, 3]

- 오른쪽 부분 배열: [9, 82, 10]

 

3. 왼쪽 부분 배열 [38, 27, 43, 3]의 분할:

- [38, 27] / [43, 3]

 

4. [38, 27]의 분할:

- [38] / [27]

- [38]과 [27]은 이미 단일 요소이므로 정렬된 상태입니다.

- [38] / [27] → [27, 38]

 

5. [43, 3]의 분할:

- [43] / [3]

- [43]과 [3]은 이미 단일 요소이므로 정렬된 상태입니다.

- [43] / [3] → [3, 43]

 

6. 병합 [27, 38] / [3, 43]:

- [27, 38] / [3, 43] → [3, 27, 38, 43]

 

7. 오른쪽 부분 배열 [9, 82, 10]의 분할:

- [9, 82] / [10]

 

8. [9, 82]의 분할:

- [9] / [82]

- [9]과 [82]는 이미 단일 요소이므로 정렬된 상태입니다.

- [9] / [82] → [9, 82]

 

9. 병합 [9, 82] / [10]:

- [9, 82] / [10] → [9, 10, 82]

 

10. 최종 병합:

- [3, 27, 38, 43] / [9, 10, 82] → [3, 9, 10, 27, 38, 43, 82]

 

최종 정렬된 배열: [3, 9, 10, 27, 38, 43, 82]

 

병합 정렬은 위와 같은 과정을 통해 리스트 전체를 정렬합니다. 분할 정복 방법은 매우 효율적이며, 큰 데이터를 처리할 때 특히 유용합니다. 이 알고리즘은 컴퓨터 과학뿐만 아니라 여러 문제를 해결할 때 사용되는 중요한 방법론 중 하나입니다.

 

 

 

 

 

구현 코드(파이썬):

def mergesort(l):
   if len(l) < 2:
        return l
   mid = len(l)//2
   left = mergesort(l[:mid])
   right = mergesort(l[mid:])
   return merge(left, right)
 

이제 우리는 코드의 마지막 부분인 merge 함수에 대해 알아야 합니다.

 

병합 merge

 

 

 

 
 

 

 
 
병합정렬의 원리 설명 이미지
 
병합정렬 유사코드

 

 

Merge의 12~17행에 있는 루프는 Merge 작동 방식의 핵심으로, 루프를 불변으로 유지합니다:

12~17행의 for 루프 반복이 시작될 때마다 하위 배열 A[p..k-1] 에는 L[1..n1 +1] 및 R[1..n2 +1]의 가장 작은 요소 k - p 개가 정렬된 순서로 들어 있습니다.

또한, L[i]와 R[j]는 배열에서 A로 다시 복사되지 않은 가장 작은 요소입니다.

 

 

합병 정렬(Merge Sort)에서의 루프 불변성(loop invariant)을 이해하기 위해서는 우선 합병 정렬이 어떻게 동작하는지 다시 한번 간단히 상기해야 합니다. 합병 정렬은 분할 정복 전략을 사용하여 리스트를 반복적으로 나누고, 정렬된 부분 리스트들을 합치는 과정을 포함합니다. 그러나 합병 정렬에서는 전형적인 for 또는 while 루프를 사용하는 대신 재귀 호출을 사용합니다. 이 재귀 호출이 루프 역할을 하며, 루프 불변성의 개념을 적용할 수 있습니다.

 

루프 불변성 정의:

루프 불변성은 루프의 각 반복에서 유지되어야 하는 조건으로, 이 조건이 참이라면 루프의 목적을 달성할 수 있습니다. 이는 루프가 시작될 때 참이며, 루프의 각 반복 후에도 계속 참으로 유지되어야 합니다.

 

합병 정렬에서의 루프 불변성:

합병 정렬에서는 재귀 함수가 루프의 역할을 하므로, 불변성은 재귀 함수의 각 호출에 대해 유지되어야 합니다.

 

1. 불변성 조건: 각 재귀 호출이 반환할 때, 해당 호출에 의해 다루어진 부분 리스트는 정렬된 상태여야 합니다.

2. 초기화: 처음 재귀 호출이 이루어질 때, 가장 작은 단위인 하나의 원소만 포함하는 리스트 또는 두 원소를 비교하여 정렬된 리스트를 반환하는 상태로 초기화됩니다. 이 상태에서는 자연스럽게 정렬이 되어 있다고 볼 수 있습니다.

3. 유지: 재귀 호출을 통해 리스트를 반으로 나누고, 각 부분에 대해 재귀적으로 정렬을 수행한 뒤, 두 정렬된 부분을 합칩니다. 두 부분이 각각 정렬된 상태로 반환되었다면, 합칠 때도 정렬된 순서를 유지하여 전체적으로 정렬된 리스트를 만듭니다. 이는 합치는 과정에서 정렬 조건을 철저히 지킴으로써 이루어집니다.

4. 종료: 최종적으로 재귀 호출이 종료될 때 모든 원소가 포함된 리스트가 반환되며, 이 리스트는 완전히 정렬된 상태입니다.

 

합병 정렬의 경우 불변성은 재귀적 구조 내에서 리스트가 정렬되어 반환된다는 확신을 제공합니다. 이로 인해, 각 단계에서 부분 리스트들이 정확하게 정렬됨을 보장하고, 이는 전체 리스트가 정렬되는 결과로 이어집니다. 이러한 불변성은 합병 정렬의 정확성과 신뢰성을 보장하는 핵심 요소입니다.

 

 

단점 : 임시의 데이터 공간이 필요합니다 -> in-place 가 아니다.

장점 : 데이터 분포에 영향을 받지 않는 편으로, 입력 데이터의 상태와 관계없이 소요 시간은 O(nlogn) 입니다. 만약 레코드를 Linked List로 구성한다면, 링크 인덱스만 변경되므로 데이터의 이동은 무시할 수 있을 정도로 작아지기 때문에 경우에 따라 in-place로 구현할 수 있습니다.

 

 

 

Chat GPT-4

합병 정렬(Merge Sort)은 재귀를 사용하여 리스트를 반으로 나누고, 나누어진 각 부분을 정렬한 후, 정렬된 부분들을 다시 합치는 방식으로 작동합니다. 파이썬에서 합병 정렬을 구현하려면 두 가지 주요 함수가 필요합니다: 하나는 리스트를 나누는 함수, 다른 하나는 정렬된 두 리스트를 합치는 함수입니다.

 

다음은 파이썬으로 작성된 합병 정렬의 전체 구현입니다:

 

def merge_sort(arr):
if len(arr) <= 1:
return arr

mid = len(arr) // 2
left_half = merge_sort(arr[:mid])
right_half = merge_sort(arr[mid:])

return merge(left_half, right_half)

def merge(left, right):
sorted_list = []
i = j = 0

# 두 부분 리스트의 원소들을 비교하여 정렬하여 합치는 과정
while i < len(left) and j < len(right):
if left[i] < right[j]:
sorted_list.append(left[i])
i += 1
else:
sorted_list.append(right[j])
j += 1

# 남은 원소들을 결과 리스트에 추가
sorted_list.extend(left[i:])
sorted_list.extend(right[j:])

return sorted_list

# 예제 사용
arr = [38, 27, 43, 3, 9, 82, 10]
sorted_arr = merge_sort(arr)
print("Sorted Array:", sorted_arr)
 

 

### 코드 설명:

1. **merge_sort 함수**:

- 입력된 리스트 `arr`의 길이가 1 이하면, 리스트는 이미 정렬된 것으로 간주하고 반환합니다.

- 리스트를 반으로 나누어 `merge_sort`를 재귀적으로 호출하여 각 부분을 정렬합니다.

 

2. **merge 함수**:

- 두 정렬된 리스트 `left`와 `right`를 입력받아, 하나의 정렬된 리스트로 합칩니다.

- 두 리스트의 원소들을 차례대로 비교하면서, 더 작은 원소를 `sorted_list`에 추가합니다.

- 한 리스트의 원소가 먼저 소진되면, 다른 리스트의 남은 원소들을 `sorted_list`에 추가합니다.

 

이 구현은 합병 정렬의 분할-정복 접근을 명확하게 보여주며, 리스트의 원소들을 효율적으로 정렬할 수 있습니다. 합병 정렬은 평균 및 최악의 경우 모두 O(n log n)의 시간 복잡도를 가집니다. 이 코드는 작은 예제를 사용해 테스트할 수 있으며, 더 큰 데이터셋에 대해서도 잘 작동합니다.