퀵 정렬 (Quick Sort)
한눈에 보기
피벗을 기준으로 배열을 분할해 재귀적으로 정렬하는 퀵 정렬을 다룬다. 평균 O(n log n)과 최악 O(n^2)의 조건, 피벗 전략, 로무토 분할 구현과 퀵 셀렉트를 정리한다.
최종 업데이트 2026.09.17
퀵 정렬(Quick Sort)은 기준값 하나를 고른 뒤 그보다 작은 값은 왼쪽, 큰 값은 오른쪽으로 몰아놓고, 양쪽을 재귀적으로 같은 방식으로 정렬하는 알고리즘이다. 이 기준값을 피벗(pivot)이라고 부른다.
평균 O(n log n)에 상수가 작아 실제로 가장 빠른 정렬 중 하나이지만, 피벗을 잘못 고르면 O(n²)까지 떨어진다. 이 "빠르지만 최악이 나쁘다"는 성질과 그 이유를 설명할 수 있는지가 면접에서 묻는 핵심이다.
개념과 원리
퀵 정렬의 한 단계는 파티션(partition)이라고 부른다. 피벗을 하나 정하고 배열을 훑어서, 피벗보다 작은 값들과 큰 값들을 갈라놓는 작업이다. 파티션이 끝나면 피벗은 최종 위치에 확정된다. 이 자리는 다시는 바뀌지 않는다.
이렇게 피벗 하나를 제자리에 놓고 나면, 왼쪽 그룹과 오른쪽 그룹을 각각 같은 방법으로 정렬하면 된다. 그룹의 크기가 1 이하가 되면 멈춘다.
파티션 과정 (Lomuto 방식)
구현이 가장 간단한 방식은 맨 뒤 원소를 피벗으로 삼고, 왼쪽부터 훑으며 피벗보다 작은 값을 앞쪽으로 모으는 것이다. 변수 i는 "지금까지 모은 작은 값 구역의 끝"을 가리킨다.
시간 복잡도
| 구분 | 복잡도 | 언제 생기는가 |
|---|---|---|
| 최선 / 평균 | O(n log n) | 피벗이 배열을 대체로 반반씩 가를 때 |
| 최악 | O(n²) | 피벗이 매번 최솟값이나 최댓값일 때 |
| 공간 | O(log n) | 재귀 호출 스택. 최악에는 O(n) |
| 안정성 | 불안정 정렬 | 멀리 있는 원소끼리 교환하므로 |
최악의 경우가 언제인지가 중요하다. 맨 뒤를 피벗으로 삼는 구현에 이미 정렬된 배열을 넣으면, 매번 피벗이 최댓값이라 한쪽 그룹이 텅 비고 다른 쪽이 n−1개가 된다. 분할이 반으로 줄지 않으니 깊이가 n이 되어 O(n²)이다. "정렬된 입력이 최악"이라는 이 역설이 면접 단골 질문이다.
구현 코드 예시
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low >= high: # 원소가 1개 이하면 정렬 끝
return arr
p = partition(arr, low, high)
quick_sort(arr, low, p - 1) # 피벗 왼쪽
quick_sort(arr, p + 1, high) # 피벗 오른쪽
return arr
def partition(arr, low, high):
pivot = arr[high] # 맨 뒤를 피벗으로
i = low - 1 # 작은 값 구역의 끝
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1] # 피벗을 제자리로
return i + 1
파이썬으로 짤 때 흔히 보이는, 리스트 컴프리헨션으로 세 조각을 만드는 방식도 있다. 읽기는 훨씬 쉽지만 매 단계 새 리스트를 만들어 O(n) 공간을 더 쓴다.
def quick_sort_simple(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
mid = [x for x in arr if x == pivot] # 중복 값을 따로 모아야 무한 재귀를 막는다
right = [x for x in arr if x > pivot]
return quick_sort_simple(left) + mid + quick_sort_simple(right)
최악을 피하는 방법
- 랜덤 피벗: 피벗을 무작위로 골라 맨 앞이나 맨 뒤와 교환한 뒤 시작한다. 특정 입력을 노린 저격을 막는 가장 간단한 방법이다.
- 중앙값 세 개 (median-of-three): 처음, 가운데, 끝 세 값 중 중간값을 피벗으로 쓴다. 이미 정렬된 입력에서 특히 효과가 좋다.
- 작은 구간은 삽입 정렬: 구간 길이가 10 남짓 이하면 재귀를 멈추고 삽입 정렬로 마무리한다. 실제 라이브러리들이 쓰는 기법이다.
코딩테스트에서 파이썬 sort()는 Timsort라 이 문제가 없지만, 직접 퀵 정렬을 구현해 제출하면 "정렬된 입력" 테스트케이스에서 시간 초과가 나기 쉽다. 직접 구현할 일이 생기면 최소한 랜덤 피벗은 넣는 것이 좋다.
퀵 셀렉트 — 정렬하지 않고 K번째 수 찾기
파티션의 부산물로 얻는 강력한 기법이다. "K번째로 작은 수"만 필요하다면 전체를 정렬할 이유가 없다. 파티션 한 번이 끝나면 피벗의 위치가 확정되므로, 찾는 순위가 피벗보다 앞인지 뒤인지 보고 한쪽만 다시 파고들면 된다.
퀵 정렬이 양쪽을 모두 재귀하는 데 반해 퀵 셀렉트는 한쪽만 본다. 그래서 평균 복잡도가 O(n log n)이 아니라 O(n)이다. 매번 절반씩 버린다고 하면 n + n/2 + n/4 + … = 2n이기 때문이다.
def quick_select(arr, k):
"""k번째로 작은 원소를 반환한다 (k는 1부터 시작)."""
low, high = 0, len(arr) - 1
target = k - 1 # 0-based 인덱스로 변환
while low <= high:
p = partition(arr, low, high) # 위에서 만든 파티션 함수를 그대로 쓴다
if p == target:
return arr[p] # 피벗이 정확히 그 자리 → 답
if p < target:
low = p + 1 # 오른쪽만 본다
else:
high = p - 1 # 왼쪽만 본다
return None
"상위 K개 구하기", "중앙값 찾기" 유형에서 정렬 O(n log n)을 O(n)으로 낮출 수 있다. 다만 최악은 여전히 O(n²)이므로 랜덤 피벗이 사실상 필수다. K가 아주 작다면 힙으로 O(n log K)에 푸는 편이 더 안정적이다.
병합 정렬과의 비교
| 구분 | 퀵 정렬 | 병합 정렬 |
|---|---|---|
| 평균 속도 | 더 빠름 (상수가 작음) | 빠름 |
| 최악 | O(n²) | O(n log n) 보장 |
| 추가 공간 | O(log n) | O(n) |
| 안정성 | 불안정 | 안정 |
| 유리한 상황 | 배열, 캐시 활용이 좋음 | 연결 리스트, 안정성이 필요할 때 |
자주 하는 실수
- 재귀 범위에 피벗을 포함시킨다.
quick_sort(arr, low, p)처럼 피벗을 다시 넣으면 구간이 줄지 않아 무한 재귀가 된다. 피벗은 이미 확정이므로 제외해야 한다. - 중복 값을 처리하지 않는다. 컴프리헨션 방식에서
<와>만 쓰고==그룹을 빼먹으면 모든 값이 같은 배열에서 원소가 사라지거나 무한 재귀에 빠진다. - 정렬된 입력을 테스트하지 않는다. 랜덤 배열로만 검증하면 최악의 경우를 놓친다.
i의 초기값을low로 둔다. 작은 값 구역이 비어 있음을 나타내려면low - 1이어야 한다.- 파이썬 재귀 한도를 안 올린다. 최악에 가까운 입력에서 깊이가 n까지 갈 수 있어
RecursionError가 난다.
면접에서 이렇게 물어본다
- 퀵 정렬의 평균과 최악 복잡도가 다른 이유를 설명해 보라.
- 이미 정렬된 배열이 퀵 정렬에 최악인 이유는 무엇인가.
- 최악의 경우를 피하는 방법을 아는 대로 말해 보라.
- 퀵 정렬이 불안정 정렬인 이유는 무엇인가.
- 병합 정렬 대신 퀵 정렬을 선택하는 기준은 무엇인가.
- 파티션 함수가 무엇을 보장하는지, 반환값의 의미가 무엇인지 설명해 보라.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- DFS와 BFS (Depth First Search, Breadth First Search)그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
- 다익스트라 알고리즘 (Dijkstra's Algorithm)음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기