비교 기반 정렬은 아무리 잘해도 O(n log n)을 넘을 수 없습니다. 하지만 비교 없이 정렬하면? 그 벽을 넘을 수 있습니다.

비교 기반 정렬의 한계

모든 비교 기반 정렬 알고리즘의 시간 복잡도 하한은 Ω(n log n) 입니다.

왜 O(n log n)인가?

n개 원소의 가능한 순서(순열)는 n!가지입니다. 비교 연산의 결과는 이진(두 가지)이므로, 이를 이진 트리(결정 트리)로 표현하면 리프가 n!개 필요합니다.

PLAINTEXT
이진 트리의 높이 ≥ log₂(n!) = Θ(n log n)

따라서 비교만으로는 O(n log n)보다 빠를 수 없습니다.

Counting Sort

값의 범위가 제한적일 때, ** 각 값의 출현 횟수를 세어** 정렬합니다.

동작 원리

PLAINTEXT
원본: [4, 2, 2, 8, 3, 3, 1]
값 범위: 0~8

1. 카운트: count = [0, 1, 2, 2, 1, 0, 0, 0, 1]
                     0  1  2  3  4  5  6  7  8

2. 누적합: count = [0, 1, 3, 5, 6, 6, 6, 6, 7]

3. 뒤에서부터 배치 (안정 정렬을 위해):
   arr[6]=1 → count[1]=1 → result[0]=1, count[1]=0
   arr[5]=3 → count[3]=5 → result[4]=3, count[3]=4
   ...

구현

JAVA
public static int[] countingSort(int[] arr, int maxVal) {
    int[] count = new int[maxVal + 1];
    int[] result = new int[arr.length];

    // 1. 각 값의 출현 횟수 세기
    for (int num : arr) {
        count[num]++;
    }

    // 2. 누적합 계산
    for (int i = 1; i <= maxVal; i++) {
        count[i] += count[i - 1];
    }

    // 3. 뒤에서부터 결과 배열에 배치 (안정 정렬)
    for (int i = arr.length - 1; i >= 0; i--) {
        result[count[arr[i]] - 1] = arr[i];
        count[arr[i]]--;
    }

    return result;
}

시간/공간 복잡도

  • 시간: O(n + k) (n: 원소 수, k: 값 범위)
  • 공간: O(n + k)

제한 사항

  • 값의 범위 k가 매우 크면 메모리 낭비 (예: 0~10억)
  • 실수(float/double)에는 직접 적용 불가
  • 음수를 처리하려면 오프셋 추가 필요

Radix Sort

각 ** 자릿수별로** 정렬을 반복하는 방식입니다. 각 자릿수 정렬에 Counting Sort를 사용합니다.

LSD (Least Significant Digit) 방식

낮은 자릿수부터 정렬합니다. 안정 정렬을 사용해야 이전 정렬 결과가 유지됩니다.

PLAINTEXT
원본:  [329, 457, 657, 839, 436, 720, 355]

1의 자리 정렬: [720, 355, 436, 457, 657, 329, 839]
10의 자리 정렬: [720, 329, 436, 839, 355, 457, 657]
100의 자리 정렬: [329, 355, 436, 457, 657, 720, 839]

구현

JAVA
public static void radixSort(int[] arr) {
    int max = Arrays.stream(arr).max().getAsInt();

    // 각 자릿수에 대해 Counting Sort 수행
    for (int exp = 1; max / exp > 0; exp *= 10) {
        countingSortByDigit(arr, exp);
    }
}

private static void countingSortByDigit(int[] arr, int exp) {
    int n = arr.length;
    int[] output = new int[n];
    int[] count = new int[10]; // 0~9

    // 해당 자릿수 기준으로 카운트
    for (int num : arr) {
        int digit = (num / exp) % 10;
        count[digit]++;
    }

    // 누적합
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }

    // 뒤에서부터 배치 (안정 정렬)
    for (int i = n - 1; i >= 0; i--) {
        int digit = (arr[i] / exp) % 10;
        output[count[digit] - 1] = arr[i];
        count[digit]--;
    }

    System.arraycopy(output, 0, arr, 0, n);
}

시간/공간 복잡도

  • 시간: O(d × (n + k)) (d: 최대 자릿수, k: 자릿수별 값 범위(보통 10))
  • 공간: O(n + k)

d가 상수이면 O(n)에 가깝습니다.

Bucket Sort

값의 분포가 균등할 때, ** 버킷으로 분류 **한 후 각 버킷을 정렬합니다.

JAVA
public static void bucketSort(float[] arr) {
    int n = arr.length;
    List<List<Float>> buckets = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        buckets.add(new ArrayList<>());
    }

    // 값을 버킷에 분배 (0 이상 1 미만의 실수 가정)
    for (float num : arr) {
        int bucketIdx = (int) (num * n);
        buckets.get(bucketIdx).add(num);
    }

    // 각 버킷을 정렬
    for (List<Float> bucket : buckets) {
        Collections.sort(bucket);
    }

    // 버킷을 합치기
    int idx = 0;
    for (List<Float> bucket : buckets) {
        for (float num : bucket) {
            arr[idx++] = num;
        }
    }
}
  • 균등 분포: O(n) (각 버킷에 상수 개의 원소)
  • 최악: O(n²) (한 버킷에 모든 원소가 몰림)

세 정렬의 비교

알고리즘시간 복잡도공간안정성적합한 경우
Counting SortO(n + k)O(n + k)안정값 범위가 작은 정수
Radix SortO(d(n + k))O(n + k)안정자릿수가 고정된 정수
Bucket SortO(n) 평균O(n + k)안정균등 분포 실수

문자열 정렬에서의 Radix Sort

문자열을 사전순으로 정렬할 때도 Radix Sort를 사용할 수 있습니다. MSD(Most Significant Digit) 방식을 사용합니다.

JAVA
// MSD Radix Sort: 높은 자릿수(첫 문자)부터 정렬
// 같은 첫 문자를 가진 문자열끼리 그룹을 만들고
// 각 그룹 내에서 두 번째 문자로 재귀적 정렬

백엔드/실무에서의 연결

데이터베이스 인덱스 정렬

DB의 B-Tree 인덱스 구축 시, 대량의 키를 정렬해야 합니다. 키의 분포 특성에 따라 Radix Sort가 사용되기도 합니다.

IP 주소 정렬

IP 주소를 정렬할 때, 각 옥텟(0~255)을 자릿수로 보면 Radix Sort가 효과적입니다.

JAVA
// IP를 정수로 변환 후 Radix Sort
// 192.168.1.1 → 3232235777

분산 정렬의 셔플 단계

MapReduce의 셔플(Shuffle) 단계에서 키를 파티셔닝할 때 Bucket Sort의 아이디어가 사용됩니다. 해시값 범위를 버킷으로 나누어 각 리듀서에 분배합니다.

로그 분석에서의 활용

로그 레벨(ERROR=0, WARN=1, INFO=2, DEBUG=3)로 분류할 때 Counting Sort가 자연스럽습니다. 레벨이 4가지뿐이므로 O(n)에 정렬됩니다.

히스토그램 생성

사용자의 나이별 분포, 요청 응답 시간별 분포 등을 계산하는 것은 Counting Sort의 1단계(카운트)와 동일합니다.

주의할 점

값의 범위가 클 때 Counting Sort를 적용하는 실수

Counting Sort는 값의 범위(k)만큼 배열을 생성합니다. k가 n보다 훨씬 크면 메모리 낭비가 심하고 오히려 비교 기반 정렬보다 느려집니다. k = O(n)일 때만 효율적입니다.

Radix Sort에서 Most Significant Digit(MSD)과 Least Significant Digit(LSD)을 혼동

LSD Radix Sort는 가장 낮은 자릿수부터 정렬하며, 안정 정렬이 전제입니다. MSD는 가장 높은 자릿수부터 정렬하며, 재귀적으로 처리해야 합니다.


정리

기억할 포인트:

  • ** 비교 기반 정렬의 하한은 O(n log n)**입니다. Counting, Radix, Bucket Sort는 비교를 하지 않으므로 이 한계를 넘을 수 있습니다.
  • Counting Sort는 ** 값 범위 k가 작을 때** 적합합니다 (O(n + k)).
  • Radix Sort는 ** 자릿수 d가 고정 **일 때 사실상 O(n)입니다.
  • Bucket Sort는 ** 균등 분포 **일 때 O(n)이지만, 최악은 O(n²)입니다.
  • 실무에서는 IP 정렬, 로그 분류, 히스토그램 등에서 이 알고리즘들의 아이디어가 활용됩니다.
댓글 로딩 중...