계수 정렬 (Counting Sort)

한눈에 보기

원소를 비교하지 않고 값별 등장 횟수로 정렬 위치를 계산하는 계수 정렬을 다룬다. O(n + k) 복잡도, 안정성을 위한 역방향 배치, 적합한 데이터 조건과 구현을 정리한다.

최종 업데이트 2026.09.17

계수 정렬(Counting Sort)은 값을 비교하지 않고 세어서 정렬하는 알고리즘이다. 각 값이 몇 번 나왔는지 센 다음, 그 개수만큼 순서대로 다시 늘어놓으면 정렬이 끝난다. 비교 기반 정렬의 이론적 한계인 O(n log n)을 뚫고 O(n + k)에 정렬할 수 있다.

대신 조건이 붙는다. 값이 정수여야 하고, 값의 범위 k가 원소 개수 n에 비해 너무 크지 않아야 한다. 코딩테스트에서 "수의 범위는 10,000 이하", "나이는 1~200" 같은 제약이 눈에 띄면 계수 정렬을 떠올릴 타이밍이다.

개념과 원리

아이디어는 단순하다. 값 0이 두 개, 값 2가 두 개, 값 3이 세 개 있다는 걸 알면, 정렬 결과는 0 0 2 2 3 3 3이다. 원소끼리 비교할 필요 없이 개수만 알면 순서가 나온다.

계수 정렬: 입력 배열의 각 값이 몇 번 등장하는지 세어 카운트 배열에 담는 과정
카운트 배열의 인덱스가 곧 값이다. 이 배열만 왼쪽부터 읽으며 개수만큼 값을 뱉으면 0 0 2 2 3 3 3 5가 되어 정렬이 끝난다.

누적합을 쓰는 이유 — 안정 정렬 만들기

위 방식은 값이 단순한 숫자일 때만 통한다. ("김", 3)처럼 값에 딸린 데이터가 있으면 개수만 세서는 원본을 복원할 수 없다. 이때 카운트 배열을 누적합으로 바꾼다.

누적합을 취하면 count[v]는 "v 이하인 원소의 총 개수"가 된다. 이 값이 곧 v가 들어갈 마지막 자리의 다음 인덱스다. 그래서 입력을 하나씩 읽으며 count[v]를 1 줄이고 그 자리에 넣으면 된다.

계수 정렬: 누적 카운트 배열을 이용해 값 3을 출력 배열의 인덱스 6에 배치하는 과정
입력을 뒤에서부터 읽는 것이 핵심이다. 뒤쪽 원소가 뒤쪽 칸을 먼저 차지하므로, 같은 값들의 원래 순서가 그대로 보존되어 안정 정렬이 된다.

시간 복잡도

구분복잡도설명
시간O(n + k)n은 원소 수, k는 값의 범위
공간O(n + k)카운트 배열 k개 + 출력 배열 n개
안정성안정 정렬누적합 + 역순 순회로 구현했을 때

k가 n보다 훨씬 크면 오히려 손해다. 원소가 10개인데 값의 범위가 10억이면 카운트 배열만 10억 칸이라 메모리부터 터진다. k가 n과 비슷하거나 작을 때만 의미가 있다.

"비교 정렬은 O(n log n)보다 빠를 수 없다"는 정리를 계수 정렬이 어긴 것처럼 보이지만, 모순이 아니다. 그 정리는 비교로만 순서를 알아내는 정렬에 대한 것이고, 계수 정렬은 값 자체를 배열 인덱스로 쓰는 추가 정보를 활용하기 때문이다.

구현 코드 예시

def counting_sort(arr, k):
    """0 이상 k 이하의 정수 배열을 정렬한다."""
    count = [0] * (k + 1)
    for v in arr:
        count[v] += 1

    for i in range(1, k + 1):          # 누적합: count[i] = i 이하인 원소 개수
        count[i] += count[i - 1]

    output = [0] * len(arr)
    for v in reversed(arr):            # 뒤에서부터 읽어야 안정 정렬이 된다
        count[v] -= 1
        output[count[v]] = v
    return output


def counting_sort_simple(arr, k):
    """값에 딸린 데이터가 없다면 세고 다시 뱉기만 해도 된다."""
    count = [0] * (k + 1)
    for v in arr:
        count[v] += 1

    result = []
    for value, c in enumerate(count):
        result.extend([value] * c)
    return result

음수가 섞여 있으면 최솟값만큼 밀어서(offset) 인덱스를 0부터 만들면 된다. count[v - min_v] += 1로 세고, 꺼낼 때 min_v를 다시 더한다.

언제 쓰는가

  • 값의 범위가 좁을 때: 나이, 점수, 학년처럼 상한이 명확한 데이터.
  • 메모리보다 속도가 급할 때: n이 수백만이어도 k가 작으면 한 번의 순회로 끝난다.
  • 기수 정렬의 부품으로: 기수 정렬은 자릿수마다 계수 정렬을 돌리는 구조다. 이때 안정성이 필수라서 누적합 방식이 반드시 필요하다.

반대로 실수, 문자열, 범위가 넓은 정수에는 쓸 수 없다. 이럴 땐 그냥 언어의 기본 정렬을 쓰는 편이 낫다.

자주 하는 실수

  • 카운트 배열 크기를 k로 잡는다. 값 k 자체를 담아야 하므로 k + 1이어야 한다. 마지막 값에서 인덱스 오류가 난다.
  • 입력을 앞에서부터 읽는다. 누적합 방식에서 정방향으로 읽으면 같은 값들의 순서가 뒤집혀 안정성이 깨진다. 기수 정렬의 부품으로 쓰면 결과 자체가 틀린다.
  • 음수를 고려하지 않는다. 음수 값이 들어오면 인덱스가 음수가 되어 파이썬에서는 조용히 배열 뒤쪽을 건드린다. 오류도 안 나서 찾기 어렵다.
  • k를 확인하지 않고 쓴다. 값 범위가 큰 데이터에 적용하면 메모리 초과가 난다. 문제의 제약 조건을 먼저 본다.

면접에서 이렇게 물어본다

  • 계수 정렬이 O(n log n)보다 빠른데도 만능이 아닌 이유는 무엇인가.
  • "비교 정렬의 하한이 O(n log n)"이라는 정리와 계수 정렬이 모순되지 않는 이유를 설명해 보라.
  • 누적합 단계가 왜 필요한가.
  • 입력을 뒤에서부터 읽어야 하는 이유는 무엇인가.
  • 음수가 포함된 배열에는 어떻게 적용하겠는가.
  • 계수 정렬을 쓰기 적합한 실제 데이터의 예를 들어 보라.
알고리즘 문서 전체 보기

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

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

공고 넣고 시작하기