계수 정렬 (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 줄이고 그 자리에 넣으면 된다.
시간 복잡도
| 구분 | 복잡도 | 설명 |
|---|---|---|
| 시간 | 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)"이라는 정리와 계수 정렬이 모순되지 않는 이유를 설명해 보라.
- 누적합 단계가 왜 필요한가.
- 입력을 뒤에서부터 읽어야 하는 이유는 무엇인가.
- 음수가 포함된 배열에는 어떻게 적용하겠는가.
- 계수 정렬을 쓰기 적합한 실제 데이터의 예를 들어 보라.
알고리즘에서 같이 읽기
- 이분 탐색 (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회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기