기수 정렬 (Radix Sort)
한눈에 보기
낮은 자릿수부터 안정 정렬을 반복해 전체를 정렬하는 기수 정렬을 다룬다. 안정성이 필수인 이유, O(d(n + k)) 복잡도와 진법 트레이드오프, 구현 코드를 정리한다.
최종 업데이트 2026.09.17
기수 정렬(Radix Sort)은 숫자를 자릿수 단위로 나눠 여러 번 정렬해서 전체를 정렬하는 알고리즘이다. 1의 자리로 한 번 정렬하고, 10의 자리로 또 정렬하고, 100의 자리로 또 정렬하다 보면 어느새 전체가 정렬되어 있다.
계수 정렬은 값의 범위 k가 크면 메모리가 터지는데, 기수 정렬은 그 약점을 해결한다. 값이 아무리 커도 자릿수 개수만큼만 반복하면 되고, 각 단계에서 필요한 카운트 배열은 0~9의 열 칸뿐이기 때문이다.
개념과 원리
가장 낮은 자리부터 시작해서 위로 올라가는 방식을 LSD(Least Significant Digit) 기수 정렬이라고 하고, 실무와 코딩테스트에서 쓰는 것은 대부분 이쪽이다. 각 단계는 그 자릿수만 보고 안정 정렬을 한 번 수행하는 것이 전부다.
한 단계 안에서 실제로 일어나는 일은 버킷에 나눠 담았다가 순서대로 다시 꺼내는 것이다. 자릿수는 0부터 9까지 열 가지뿐이므로 버킷도 열 개면 충분하다.
왜 낮은 자리부터인가
기수 정렬에서 가장 헷갈리는 부분이다. 직관적으로는 큰 자리부터 정렬해야 할 것 같은데, LSD 방식은 반대로 간다. 답은 안정 정렬에 있다.
10의 자리로 정렬하는 단계를 보자. 24와 45는 10의 자리가 각각 2와 4로 다르니 순서가 정해진다. 그런데 170과 75는 10의 자리가 둘 다 7로 같다. 이때 안정 정렬은 이전 단계의 순서를 그대로 유지한다. 이전 단계에서 1의 자리로 이미 정렬해뒀으므로, 170(1의 자리 0)이 75(1의 자리 5)보다 앞에 오는 순서가 보존된다.
즉 "위 자리가 같으면 아래 자리 순서를 따른다"는 규칙이 안정 정렬 덕분에 공짜로 지켜진다. 이게 낮은 자리부터 올라가는 이유이고, 각 단계에 반드시 안정 정렬(보통 계수 정렬)을 써야 하는 이유다. 불안정 정렬을 쓰면 이전 단계 결과가 뒤섞여 전부 무너진다.
시간 복잡도
| 구분 | 복잡도 | 설명 |
|---|---|---|
| 시간 | O(d × (n + b)) | d는 자릿수, b는 진법(10진수면 10) |
| 실질적으로 | O(dn) | b가 상수라 자릿수 × 원소 수 |
| 공간 | O(n + b) | 출력 배열 + 버킷 |
| 안정성 | 안정 정렬 | 안정 정렬을 부품으로 써야만 성립 |
d를 상수로 보면 O(n)이라 비교 정렬보다 빠르다. 하지만 값이 커지면 d도 커진다. 값의 최댓값이 m일 때 d는 대략 log m이므로, 실제로는 O(n log m)에 가깝다. n이 작고 값이 아주 큰 경우에는 O(n log n) 정렬이 오히려 낫다.
구현 코드 예시
def counting_sort_by_digit(arr, exp):
"""exp 자릿수(1, 10, 100…)를 기준으로 안정 정렬한다."""
n = len(arr)
output = [0] * n
count = [0] * 10 # 0~9 열 칸이면 충분하다
for v in arr:
count[(v // exp) % 10] += 1
for i in range(1, 10): # 누적합
count[i] += count[i - 1]
for v in reversed(arr): # 뒤에서부터 — 안정성의 핵심
digit = (v // exp) % 10
count[digit] -= 1
output[count[digit]] = v
return output
def radix_sort(arr):
if not arr:
return arr
exp = 1
max_v = max(arr)
while max_v // exp > 0: # 최댓값의 자릿수만큼 반복
arr = counting_sort_by_digit(arr, exp)
exp *= 10
return arr
(v // exp) % 10이 자릿수를 뽑는 관용구다. exp=1이면 1의 자리, exp=10이면 10의 자리가 나온다. 반복 조건을 max_v // exp > 0으로 두면 최댓값의 자릿수만큼만 돌고 자동으로 멈춘다.
한계와 대안
- 음수를 그대로 다루지 못한다. 자릿수 추출이 음수에서 깨진다. 음수와 양수를 분리해 각각 정렬한 뒤, 음수 쪽은 뒤집어서 앞에 붙이는 방법을 쓴다.
- 실수에 쓰기 어렵다. 소수점 이하 자릿수가 정해져 있지 않으면 d가 무한해진다.
- 메모리를 계속 쓴다. 자릿수마다 출력 배열을 새로 만든다. 제자리 정렬이 아니다.
- 문자열에는 쓸 수 있다. 길이가 같은 문자열이라면 마지막 글자부터 같은 방식으로 정렬하면 사전순 정렬이 된다.
코딩테스트에서 기수 정렬을 직접 구현해 제출할 일은 거의 없다. 다만 "정렬을 O(n)에 하라"거나 "비교 연산을 쓰지 말라"는 제약이 붙으면 계수 정렬과 함께 후보에 올려야 한다.
자주 하는 실수
- 불안정 정렬을 부품으로 쓴다. 각 단계에 퀵 정렬 같은 불안정 정렬을 쓰면 이전 자릿수 순서가 무너져 결과가 틀린다. 기수 정렬의 전제가 깨지는 것이라 부분적으로 맞는 답조차 안 나온다.
- 높은 자리부터 정렬한다. LSD 방식에서 순서를 뒤집으면 아래 자리가 위 자리를 덮어써서 완전히 틀린 결과가 나온다.
- 반복 횟수를 잘못 잡는다. 원소 개수가 아니라 최댓값의 자릿수만큼 돌아야 한다.
- 계수 정렬 단계에서 정방향으로 읽는다.
reversed(arr)를 빼면 안정성이 깨져 위와 같은 문제가 생긴다. - 음수를 그냥 넣는다. 파이썬에서
(-45 // 10) % 10은 예상과 다른 값을 낸다. 별도 처리가 필요하다.
면접에서 이렇게 물어본다
- 기수 정렬이 낮은 자리부터 정렬하는 이유를 설명해 보라.
- 각 단계에 반드시 안정 정렬을 써야 하는 이유는 무엇인가.
- 계수 정렬과 기수 정렬의 관계는 무엇인가.
- 기수 정렬이 O(n)에 가까운데도 기본 정렬로 쓰이지 않는 이유는 무엇인가.
- 음수가 포함된 배열은 어떻게 처리하겠는가.
- 문자열 정렬에 기수 정렬을 적용할 수 있는가.
알고리즘에서 같이 읽기
- 이분 탐색 (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회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기