기수 정렬 (Radix Sort)

한눈에 보기

낮은 자릿수부터 안정 정렬을 반복해 전체를 정렬하는 기수 정렬을 다룬다. 안정성이 필수인 이유, O(d(n + k)) 복잡도와 진법 트레이드오프, 구현 코드를 정리한다.

최종 업데이트 2026.09.17

기수 정렬(Radix Sort)은 숫자를 자릿수 단위로 나눠 여러 번 정렬해서 전체를 정렬하는 알고리즘이다. 1의 자리로 한 번 정렬하고, 10의 자리로 또 정렬하고, 100의 자리로 또 정렬하다 보면 어느새 전체가 정렬되어 있다.

계수 정렬은 값의 범위 k가 크면 메모리가 터지는데, 기수 정렬은 그 약점을 해결한다. 값이 아무리 커도 자릿수 개수만큼만 반복하면 되고, 각 단계에서 필요한 카운트 배열은 0~9의 열 칸뿐이기 때문이다.

개념과 원리

가장 낮은 자리부터 시작해서 위로 올라가는 방식을 LSD(Least Significant Digit) 기수 정렬이라고 하고, 실무와 코딩테스트에서 쓰는 것은 대부분 이쪽이다. 각 단계는 그 자릿수만 보고 안정 정렬을 한 번 수행하는 것이 전부다.

기수 정렬: 여덟 개 수를 1의 자리, 10의 자리, 100의 자리 순으로 세 번 정렬하면 전체가 정렬되는 과정
각 행 아래 작은 숫자가 그 단계에서 기준으로 삼은 자릿수다. 자릿수만 보고 정렬했는데 세 번 만에 전체가 정렬됐다. 가장 큰 수 802의 자릿수가 3이라 3번이면 충분하다.

한 단계 안에서 실제로 일어나는 일은 버킷에 나눠 담았다가 순서대로 다시 꺼내는 것이다. 자릿수는 0부터 9까지 열 가지뿐이므로 버킷도 열 개면 충분하다.

기수 정렬: 1의 자리 값에 따라 여덟 개 수를 0번부터 9번까지의 버킷에 나눠 담고 순서대로 꺼내 이어 붙이는 그림
1의 자리 기준으로 나눠 담은 모습. 같은 버킷 안에서는 담긴 순서가 그대로 유지되고, 꺼낼 때도 그 순서대로 꺼낸다. 이것이 각 단계가 안정 정렬이 되는 방식이다.

왜 낮은 자리부터인가

기수 정렬에서 가장 헷갈리는 부분이다. 직관적으로는 큰 자리부터 정렬해야 할 것 같은데, 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)에 가까운데도 기본 정렬로 쓰이지 않는 이유는 무엇인가.
  • 음수가 포함된 배열은 어떻게 처리하겠는가.
  • 문자열 정렬에 기수 정렬을 적용할 수 있는가.
알고리즘 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기