티스토리 뷰
참고
- 「취업을 위한 코딩 테스트이다 with 파이썬」 나동빈 지음
- 「Do it! 자료구조와 함께 배우는 알고리즘 입문 파이썬 편」 시바타 보요 지음
- https://yabmoons.tistory.com/250
콘텐츠
- 개념
- 파이썬 구현 코드
- 시간 복잡도
- 장단점
- 퀵 정렬과 병합 정렬 선택 기준
대부분의 프로그래밍 언어에서 정렬 라이브러리는 속도가 빠른 퀵이나 병합 정렬을 기반으로 한다.
퀵 정렬 (Quick Sort)
- 개념
피벗pivot이라는 기준에 의해 리스트 내 큰 숫자와 작은 숫자를 교환한다. 피벗을 설정하는 기준은 다양하지만, 리스트의 첫 번째 원소를 피벗으로 설정하는 "호어 분할 방식"이 가장 대표적이다.
초록색이 피벗이고, 노란색 화살표는 5 다음 숫자에서부터 피벗보다 큰 값을 찾고, 파란색 화살표는 리스트 끝에서부터 피벗보다 작은 값을 찾는다. 각 숫자에 도달하면 두 화살표가 가리키는 숫자를 교환한 후 계속 탐색한다. 그러다가 두 화살표가 엇갈리면, 피벗과 더 작은 값을 가리키는 화살표끼리 교환한다. 그럼 피벗은 해당 위치에 정렬된다.
- 파이썬 구현 코드
def quick(data, start, end):
if start >= end:
return
pivot = start
left = start + 1
right = end
while left <= right:
while left <= end and data[left] <= data[pivot]:
left += 1
while right > start and data[right] >= data[pivot]:
right -= 1
if left > right:
data[pivot], data[right] = data[right], data[pivot]
else:
data[left], data[right] = data[right], data[left]
quick(data, start, right - 1)
quick(data, right + 1, end)
- 시간 복잡도
피벗에 따라 시간 복잡도가 달라진다. 평균 시간 복잡도는 O(n log n)이다. 분할 과정에서 log n 시간이 걸리고, 각 분할 과정에서마다 n개의 데이터를 비교하기 때문이다.
그러나 이미 리스트가 정렬되어 있는 최악의 경우에는 O(n²) 시간 복잡도를 갖는다. 왜냐하면 1, 2, 3, 4가 있을 때 1과 2, 3, 4 / 2와 3, 4 / 3과 4처럼 계속 원소 1개와 나머지로 나뉘기 때문에 n번의 분할이 발생하기 때문이다.
한편 C++을 비롯한 많은 프로그래밍 언어의 정렬 라이브러리가 퀵 정렬을 기반으로 하는데, 이곳에는 최악의 경우에도 O(n log n)을 보장하기 위해 피벗 설정 시 추가적인 작업을 거친다.
- 장단점
- 속도가 빠르다. 똑같은 O(n log n) 시간 복잡도를 가진 다른 정렬 알고리즘보다도 속도가 빠른 편이다.
- 피벗 설정 방식에 따라 시간 복잡도가 O(n)이 될 수도 있다.
병합 정렬 (합병 정렬, Merge Sort)
- 개념
분할과 정복 기법이 적용된 정렬 방법이다. 리스트 내 n개의 데이터를 n / 2로 두 리스트로 나누고, 이들을 각각 정렬한 뒤 병합한다.
위 그림에서 파란색 화살표가 한 리스트를 두 개의 리스트로 나누는 '분할'이고, 각 리스트를 정렬하는 것은 '정복', 초록색 화살표는 두 리스트를 하나로 합치는 '병합/합병' 작업이다.
- 파이썬 구현 코드
def merge_sort(array):
if len(array) < 2:
return array
mid = len(array) // 2
left = merge_sort(array[:mid])
right = merge_sort(array[mid:])
return merge(left, right)
def merge(left, right):
result = []
l = r = 0
while l < len(left) and r < len(right):
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
result += left[l:]
result += right[r:]
return result
- 시간 복잡도
리스트를 정렬하는 과정은 리스트를 계속 반으로 쪼개어 나가기 때문에 O(log n) 시간 복잡도를 갖는다. 정렬된 두 리스트를 병합하는 과정의 시간 복잡도는 left 리스트의 원소 개수 l, right 리스트의 원소 개수 r의 합인 O(l + r)이다. 그러나 합병되는 두 리스트는 결국 최종적으로 기존 n개의 데이터를 가진 리스트이므로, 시간 복잡도 역시 O(n)이다. 따라서 병합 정렬의 최종 시간 복잡도는 O(n log n)이다.
- 장단점
- 똑같은 시간 복잡도를 가진 퀵 정렬은 피벗에 따라 속도가 저하될 수 있지만, 병합 정렬은 늘 빠른 속도로 정렬한다.
- 병합 과정에서 result라는 임시 리스트를 사용한 것처럼 추가적인 메모리 할당이 필요하다.
퀵 정렬과 병합 정렬 선택 기준
퀵 정렬은 제자리 정렬로 추가적인 메모리 사용이 없지만, 피벗을 어떻게 설정하는가에 따라 성능 편차가 발생한다.
병합 정렬은 정렬된 값을 담는 임시 리스트 때문에 추가적인 메모리가 사용되지만, 늘 O(n log n) 시간 복잡도를 보장한다.
따라서 언제, 무슨 정렬 방법을 사용하는 기준은 "메모리 추가 사용 가능 여부"와 "데이터의 상태"이다.
예를 들어 데이터가 정렬된 최악의 상황에서는 퀵 정렬은 O(n²), 병합 정렬은 O(n log n)이므로 병합 정렬을 사용하는 것이 옳다. 즉, 일반적으론 병합 정렬을 선택하는 것이 최선의 선택일 것이다.
그러나 만약 추가적인 데이터 할당이 불가능한 상황이라면, in-place 정렬인 퀵 정렬을 사용할 수밖에 없다.
'학습 > 알고리즘' 카테고리의 다른 글
유클리드 알고리즘: 최대공약수, 최소공배수 구하기 (0) | 2023.07.03 |
---|---|
정렬 알고리즘1: 버블, 선택, 삽입 (0) | 2023.06.21 |