퀵 정렬(Quick Sort)은 분할 정복(Divide and Conquer) 알고리즘을 기반으로 한 정렬 방법 중 하나다. 평균적으로 매우 빠른 성능을 보이며, 이번 글에서는 퀵 정렬의 원리와 구현 방법(재귀적, 비재귀적 방식) 그리고 시간 복잡도와 안정성까지 정리해보겠다.
퀵 정렬은 리스트를 정렬하는 알고리즘 중 하나로, 기준이 되는 원소(Pivot)를 설정하고 이를 기준으로 왼쪽에는 작은 값, 오른쪽에는 큰 값을 배치하는 과정을 반복하여 정렬을 수행한다. 일반적으로 매우 빠른 속도를 보이며, 평균적으로 O(n log n)의 시간 복잡도를 가진다.
퀵 정렬은 다음과 같은 원리로 동작한다:
이 과정을 반복하면 전체 배열이 정렬된다.
from typing import MutableSequence
def qsort(a: MutableSequence) -> MutableSequence:
if len(a) <= 1:
return a
pivot = a[len(a) // 2] # 피벗을 배열의 가운데 요소로 선택
left = [x for x in a if x < pivot] # 피벗보다 작은 값들
middle = [x for x in a if x == pivot] # 피벗과 같은 값들
right = [x for x in a if x > pivot] # 피벗보다 큰 값들
return quick_sort_recursive(left) + middle + quick_sort_recursive(right)
def qsort(a: MutableSequence, left: int, right: int) -> None:
pl = left
pr = right
x = a[(left + right) // 2] # 피벗을 배열의 가운데 요소로 선택
while pl <= pr:
while a[pl] < x: pl += 1 # 왼쪽에서 피벗보다 큰 값 찾기
while a[pr] > x: pr -= 1 # 오른쪽에서 피벗보다 작은 값 찾기
if pl <= pr: # 교차되지 않았다면 교환
a[pl], a[pr] = a[pr], a[pl]
pl += 1
pr -= 1
# 재귀적으로 왼쪽과 오른쪽 부분 정렬
if left < pr: qsort(a, left, pr)
if pl < right: qsort(a, pl, right)
재귀적인 퀵 정렬은 함수 호출이 중첩되므로 스택 오버플로우가 발생할 가능성이 있다. 이를 방지하기 위해 명시적으로 스택을 사용하여 비재귀적인 방법으로 구현할 수 있다.
from typing import MutableSequence
def qsort(a: MutableSequence, left: int, right: int) -> None:
stack = [(left, right)]
while stack:
pl, pr = left, right = stack.pop()
x = a[(left + right) // 2]
while pl <= pr:
while a[pl] < x: pl += 1
while a[pr] > x: pr -= 1
if pl <= pr:
a[pl], a[pr] = a[pr], a[pl]
pl += 1
pr -= 1
if left < pr:
stack.append((left, pr))
if pl < right:
stack.append((pl, right))
| 경우 | 시간 복잡도 |
|---|---|
| 평균 | O(n log n) |
| 최선 | O(n log n) |
| 최악 | O(n^2) |
O(n log n)의 성능을 보인다.O(n^2)까지 성능이 저하될 수 있다.O(n log n)의 성능을 보장한다.O(n^2)이 될 가능성이 있기 때문에 적절한 피벗 선택 전략이 필요하다.