콘텐츠로 이동

11.9   계수 정렬

계수 정렬(counting sort)은 원소의 개수를 세어 정렬하며, 주로 정수 배열에 사용한다.

11.9.1   간단한 구현

먼저 간단한 예를 보자. 길이가 \(n\)인 배열 nums의 모든 원소가 '음이 아닌 정수'라고 하자. 계수 정렬의 전체 과정은 아래 그림(11-16)과 같다.

  1. 배열을 순회하여 가장 큰 수 \(m\)을 찾고, 길이가 \(m + 1\)인 보조 배열 counter를 만든다.
  2. counter를 이용하여 nums에서 각 수가 나타나는 횟수를 센다. counter[num]은 수 num의 등장 횟수다. 세는 방법은 간단하다. nums를 순회하면서 현재 수를 num이라고 할 때 counter[num]을 \(1\)씩 늘리면 된다.
  3. counter의 인덱스는 본래 순서대로 배열되어 있으므로, 모든 수가 이미 정렬된 셈이다. 이어서 counter를 순회하며 각 수의 등장 횟수만큼 작은 수부터 큰 수 순서로 nums에 채우면 된다.

계수 정렬 과정

그림 11-16   계수 정렬 과정

코드는 다음과 같다.

counting_sort.py
def counting_sort_naive(nums: list[int]):
    """계수 정렬"""
    # 간단한 구현이며, 객체 정렬에는 사용할 수 없다
    # 1. 배열의 최댓값 m을 구한다
    m = max(nums)
    # 2. 각 숫자의 등장 횟수를 센다
    # counter[num]은 num의 등장 횟수를 나타낸다
    counter = [0] * (m + 1)
    for num in nums:
        counter[num] += 1
    # 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    i = 0
    for num in range(m + 1):
        for _ in range(counter[num]):
            nums[i] = num
            i += 1
counting_sort.cpp
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
void countingSortNaive(vector<int> &nums) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    for (int num : nums) {
        m = max(m, num);
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    vector<int> counter(m + 1, 0);
    for (int num : nums) {
        counter[num]++;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    int i = 0;
    for (int num = 0; num < m + 1; num++) {
        for (int j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
}
counting_sort.java
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
void countingSortNaive(int[] nums) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    for (int num : nums) {
        m = Math.max(m, num);
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    int[] counter = new int[m + 1];
    for (int num : nums) {
        counter[num]++;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    int i = 0;
    for (int num = 0; num < m + 1; num++) {
        for (int j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
}
counting_sort.cs
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
void CountingSortNaive(int[] nums) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    foreach (int num in nums) {
        m = Math.Max(m, num);
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    int[] counter = new int[m + 1];
    foreach (int num in nums) {
        counter[num]++;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    int i = 0;
    for (int num = 0; num < m + 1; num++) {
        for (int j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
}
counting_sort.go
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
func countingSortNaive(nums []int) {
    // 1. 배열의 최댓값 m을 구한다
    m := 0
    for _, num := range nums {
        if num > m {
            m = num
        }
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    counter := make([]int, m+1)
    for _, num := range nums {
        counter[num]++
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    for i, num := 0, 0; num < m+1; num++ {
        for j := 0; j < counter[num]; j++ {
            nums[i] = num
            i++
        }
    }
}
counting_sort.swift
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
func countingSortNaive(nums: inout [Int]) {
    // 1. 배열의 최댓값 m을 구한다
    let m = nums.max()!
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    var counter = Array(repeating: 0, count: m + 1)
    for num in nums {
        counter[num] += 1
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    var i = 0
    for num in 0 ..< m + 1 {
        for _ in 0 ..< counter[num] {
            nums[i] = num
            i += 1
        }
    }
}
counting_sort.js
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
function countingSortNaive(nums) {
    // 1. 배열의 최댓값 m을 구한다
    let m = Math.max(...nums);
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    const counter = new Array(m + 1).fill(0);
    for (const num of nums) {
        counter[num]++;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    let i = 0;
    for (let num = 0; num < m + 1; num++) {
        for (let j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
}
counting_sort.ts
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
function countingSortNaive(nums: number[]): void {
    // 1. 배열의 최댓값 m을 구한다
    let m: number = Math.max(...nums);
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    const counter: number[] = new Array<number>(m + 1).fill(0);
    for (const num of nums) {
        counter[num]++;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    let i = 0;
    for (let num = 0; num < m + 1; num++) {
        for (let j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
}
counting_sort.dart
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
void countingSortNaive(List<int> nums) {
  // 1. 배열의 최댓값 m을 구한다
  int m = 0;
  for (int _num in nums) {
    m = max(m, _num);
  }
  // 2. 각 숫자의 등장 횟수를 센다
  // counter[_num]은 _num의 등장 횟수를 나타낸다
  List<int> counter = List.filled(m + 1, 0);
  for (int _num in nums) {
    counter[_num]++;
  }
  // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
  int i = 0;
  for (int _num = 0; _num < m + 1; _num++) {
    for (int j = 0; j < counter[_num]; j++, i++) {
      nums[i] = _num;
    }
  }
}
counting_sort.rs
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
fn counting_sort_naive(nums: &mut [i32]) {
    // 1. 배열의 최댓값 m을 구한다
    let m = *nums.iter().max().unwrap();
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    let mut counter = vec![0; m as usize + 1];
    for &num in nums.iter() {
        counter[num as usize] += 1;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    let mut i = 0;
    for num in 0..m + 1 {
        for _ in 0..counter[num as usize] {
            nums[i] = num;
            i += 1;
        }
    }
}
counting_sort.c
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
void countingSortNaive(int nums[], int size) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    for (int i = 0; i < size; i++) {
        if (nums[i] > m) {
            m = nums[i];
        }
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    int *counter = calloc(m + 1, sizeof(int));
    for (int i = 0; i < size; i++) {
        counter[nums[i]]++;
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    int i = 0;
    for (int num = 0; num < m + 1; num++) {
        for (int j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
    // 4. 메모리를 해제한다
    free(counter);
}
counting_sort.kt
/* 계수 정렬 */
// 간단한 구현이며, 객체 정렬에는 사용할 수 없다
fun countingSortNaive(nums: IntArray) {
    // 1. 배열의 최댓값 m을 구한다
    var m = 0
    for (num in nums) {
        m = max(m, num)
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    val counter = IntArray(m + 1)
    for (num in nums) {
        counter[num]++
    }
    // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
    var i = 0
    for (num in 0..<m + 1) {
        var j = 0
        while (j < counter[num]) {
            nums[i] = num
            j++
            i++
        }
    }
}
counting_sort.rb
### 계수 정렬 ###
def counting_sort_naive(nums)
  # 간단한 구현이며, 객체 정렬에는 사용할 수 없다
  # 1. 배열의 최댓값 m을 구한다
  m = 0
  nums.each { |num| m = [m, num].max }
  # 2. 각 숫자의 등장 횟수를 센다
  # counter[num]은 num의 등장 횟수를 나타낸다
  counter = Array.new(m + 1, 0)
  nums.each { |num| counter[num] += 1 }
  # 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다
  i = 0
  for num in 0...(m + 1)
    (0...counter[num]).each do
      nums[i] = num
      i += 1
    end
  end
end
코드 시각화

계수 정렬과 버킷 정렬의 관계

버킷 정렬의 관점에서는 계수 배열 counter의 각 인덱스를 하나의 버킷으로, 개수를 세는 과정을 원소를 해당 버킷에 분배하는 과정으로 볼 수 있다. 본질적으로 계수 정렬은 정수 데이터에 적용되는 버킷 정렬의 특수한 경우다.

11.9.2   완전한 구현

주의 깊게 읽었다면 입력 데이터가 객체일 때 위의 3. 단계가 작동하지 않는다는 것을 알아차렸을 것이다. 입력이 상품 객체이고 상품 가격(클래스의 멤버 변수)에 따라 정렬하려 한다고 하자. 위 알고리즘으로는 가격의 정렬 결과만 얻을 수 있다.

그렇다면 원래 데이터의 정렬 결과는 어떻게 얻을까? 먼저 counter의 '접두 합'을 계산한다. 이름 그대로 인덱스 i의 접두 합 prefix[i]는 배열의 앞 i 개 원소를 더한 값이다.

\[ \text{prefix}[i] = \sum_{j=0}^i \text{counter[j]} \]

접두 합은 명확한 의미를 가진다. prefix[num] - 1은 결과 배열 res에서 원소 num이 마지막으로 나타나는 인덱스다. 이는 각 원소가 결과 배열의 어디에 놓여야 하는지 알려 주는 핵심 정보다. 이어서 원래 배열 nums의 각 원소 num을 역순으로 순회하며 매번 다음 두 단계를 수행한다.

  1. 배열 res의 인덱스 prefix[num] - 1에 num을 넣는다.
  2. 접두 합 prefix[num]을 \(1\) 줄여 다음에 num을 넣을 인덱스를 얻는다.

순회를 마치면 배열 res에 정렬된 결과가 들어 있다. 마지막으로 res를 원래 배열 nums에 덮어쓰면 된다. 아래 그림(11-17)은 계수 정렬의 전체 과정을 보여 준다.

계수 정렬 단계

계수 정렬 2단계

계수 정렬 3단계

계수 정렬 4단계

계수 정렬 5단계

계수 정렬 6단계

계수 정렬 7단계

계수 정렬 8단계

그림 11-17   계수 정렬 단계

계수 정렬의 구현 코드는 다음과 같다.

counting_sort.py
def counting_sort(nums: list[int]):
    """계수 정렬"""
    # 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
    # 1. 배열의 최댓값 m을 구한다
    m = max(nums)
    # 2. 각 숫자의 등장 횟수를 센다
    # counter[num]은 num의 등장 횟수를 나타낸다
    counter = [0] * (m + 1)
    for num in nums:
        counter[num] += 1
    # 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    # 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for i in range(m):
        counter[i + 1] += counter[i]
    # 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    # 결과를 기록할 배열 res를 초기화한다
    n = len(nums)
    res = [0] * n
    for i in range(n - 1, -1, -1):
        num = nums[i]
        res[counter[num] - 1] = num  # num을 해당 인덱스에 배치한다
        counter[num] -= 1  # 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    # 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for i in range(n):
        nums[i] = res[i]
counting_sort.cpp
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
void countingSort(vector<int> &nums) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    for (int num : nums) {
        m = max(m, num);
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    vector<int> counter(m + 1, 0);
    for (int num : nums) {
        counter[num]++;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (int i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    int n = nums.size();
    vector<int> res(n);
    for (int i = n - 1; i >= 0; i--) {
        int num = nums[i];
        res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num]--;              // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    nums = res;
}
counting_sort.java
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
void countingSort(int[] nums) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    for (int num : nums) {
        m = Math.max(m, num);
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    int[] counter = new int[m + 1];
    for (int num : nums) {
        counter[num]++;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (int i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    int n = nums.length;
    int[] res = new int[n];
    for (int i = n - 1; i >= 0; i--) {
        int num = nums[i];
        res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for (int i = 0; i < n; i++) {
        nums[i] = res[i];
    }
}
counting_sort.cs
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
void CountingSort(int[] nums) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    foreach (int num in nums) {
        m = Math.Max(m, num);
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    int[] counter = new int[m + 1];
    foreach (int num in nums) {
        counter[num]++;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (int i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    int n = nums.Length;
    int[] res = new int[n];
    for (int i = n - 1; i >= 0; i--) {
        int num = nums[i];
        res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for (int i = 0; i < n; i++) {
        nums[i] = res[i];
    }
}
counting_sort.go
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
func countingSort(nums []int) {
    // 1. 배열의 최댓값 m을 구한다
    m := 0
    for _, num := range nums {
        if num > m {
            m = num
        }
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    counter := make([]int, m+1)
    for _, num := range nums {
        counter[num]++
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for i := 0; i < m; i++ {
        counter[i+1] += counter[i]
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    n := len(nums)
    res := make([]int, n)
    for i := n - 1; i >= 0; i-- {
        num := nums[i]
        // num을 해당 인덱스에 배치한다
        res[counter[num]-1] = num
        // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
        counter[num]--
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    copy(nums, res)
}
counting_sort.swift
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
func countingSort(nums: inout [Int]) {
    // 1. 배열의 최댓값 m을 구한다
    let m = nums.max()!
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    var counter = Array(repeating: 0, count: m + 1)
    for num in nums {
        counter[num] += 1
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for i in 0 ..< m {
        counter[i + 1] += counter[i]
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    var res = Array(repeating: 0, count: nums.count)
    for i in nums.indices.reversed() {
        let num = nums[i]
        res[counter[num] - 1] = num // num을 해당 인덱스에 배치한다
        counter[num] -= 1 // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for i in nums.indices {
        nums[i] = res[i]
    }
}
counting_sort.js
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
function countingSort(nums) {
    // 1. 배열의 최댓값 m을 구한다
    let m = Math.max(...nums);
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    const counter = new Array(m + 1).fill(0);
    for (const num of nums) {
        counter[num]++;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (let i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    const n = nums.length;
    const res = new Array(n);
    for (let i = n - 1; i >= 0; i--) {
        const num = nums[i];
        res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for (let i = 0; i < n; i++) {
        nums[i] = res[i];
    }
}
counting_sort.ts
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
function countingSort(nums: number[]): void {
    // 1. 배열의 최댓값 m을 구한다
    let m: number = Math.max(...nums);
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    const counter: number[] = new Array<number>(m + 1).fill(0);
    for (const num of nums) {
        counter[num]++;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (let i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    const n = nums.length;
    const res: number[] = new Array<number>(n);
    for (let i = n - 1; i >= 0; i--) {
        const num = nums[i];
        res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for (let i = 0; i < n; i++) {
        nums[i] = res[i];
    }
}
counting_sort.dart
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
void countingSort(List<int> nums) {
  // 1. 배열의 최댓값 m을 구한다
  int m = 0;
  for (int _num in nums) {
    m = max(m, _num);
  }
  // 2. 각 숫자의 등장 횟수를 센다
  // counter[_num]은 _num의 등장 횟수를 나타낸다
  List<int> counter = List.filled(m + 1, 0);
  for (int _num in nums) {
    counter[_num]++;
  }
  // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
  // 즉, counter[_num]-1은 res에서 _num이 마지막으로 등장하는 인덱스이다
  for (int i = 0; i < m; i++) {
    counter[i + 1] += counter[i];
  }
  // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
  // 결과를 기록할 배열 res를 초기화한다
  int n = nums.length;
  List<int> res = List.filled(n, 0);
  for (int i = n - 1; i >= 0; i--) {
    int _num = nums[i];
    res[counter[_num] - 1] = _num; // _num을 해당 인덱스에 배치한다
    counter[_num]--; // 접두 합을 1 감소시켜 다음에 _num을 배치할 인덱스를 구한다
  }
  // 결과 배열 res로 원래 배열 nums를 덮어쓴다
  nums.setAll(0, res);
}
counting_sort.rs
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
fn counting_sort(nums: &mut [i32]) {
    // 1. 배열의 최댓값 m을 구한다
    let m = *nums.iter().max().unwrap() as usize;
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    let mut counter = vec![0; m + 1];
    for &num in nums.iter() {
        counter[num as usize] += 1;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for i in 0..m {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    let n = nums.len();
    let mut res = vec![0; n];
    for i in (0..n).rev() {
        let num = nums[i];
        res[counter[num as usize] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num as usize] -= 1; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    nums.copy_from_slice(&res)
}
counting_sort.c
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
void countingSort(int nums[], int size) {
    // 1. 배열의 최댓값 m을 구한다
    int m = 0;
    for (int i = 0; i < size; i++) {
        if (nums[i] > m) {
            m = nums[i];
        }
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    int *counter = calloc(m, sizeof(int));
    for (int i = 0; i < size; i++) {
        counter[nums[i]]++;
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (int i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    int *res = malloc(sizeof(int) * size);
    for (int i = size - 1; i >= 0; i--) {
        int num = nums[i];
        res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다
        counter[num]--;              // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    memcpy(nums, res, size * sizeof(int));
    // 5. 메모리를 해제한다
    free(res);
    free(counter);
}
counting_sort.kt
/* 계수 정렬 */
// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
fun countingSort(nums: IntArray) {
    // 1. 배열의 최댓값 m을 구한다
    var m = 0
    for (num in nums) {
        m = max(m, num)
    }
    // 2. 각 숫자의 등장 횟수를 센다
    // counter[num]은 num의 등장 횟수를 나타낸다
    val counter = IntArray(m + 1)
    for (num in nums) {
        counter[num]++
    }
    // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
    // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
    for (i in 0..<m) {
        counter[i + 1] += counter[i]
    }
    // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
    // 결과를 기록할 배열 res를 초기화한다
    val n = nums.size
    val res = IntArray(n)
    for (i in n - 1 downTo 0) {
        val num = nums[i]
        res[counter[num] - 1] = num // num을 해당 인덱스에 배치한다
        counter[num]-- // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
    }
    // 결과 배열 res로 원래 배열 nums를 덮어쓴다
    for (i in 0..<n) {
        nums[i] = res[i]
    }
}
counting_sort.rb
### 계수 정렬 ###
def counting_sort(nums)
  # 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다
  # 1. 배열의 최댓값 m을 구한다
  m = nums.max
  # 2. 각 숫자의 등장 횟수를 센다
  # counter[num]은 num의 등장 횟수를 나타낸다
  counter = Array.new(m + 1, 0)
  nums.each { |num| counter[num] += 1 }
  # 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다
  # 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다
  (0...m).each { |i| counter[i + 1] += counter[i] }
  # 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다
  # 결과를 기록할 배열 res를 초기화한다
  n = nums.length
  res = Array.new(n, 0)
  (n - 1).downto(0).each do |i|
    num = nums[i]
    res[counter[num] - 1] = num # num을 해당 인덱스에 배치한다
    counter[num] -= 1 # 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다
  end
  # 결과 배열 res로 원래 배열 nums를 덮어쓴다
  (0...n).each { |i| nums[i] = res[i] }
end
코드 시각화

11.9.3   알고리즘 특성

  • 시간 복잡도 \(O(n + m)\), 비적응형 정렬: nums와 counter를 순회하며, 각각 선형 시간이 걸린다. 보통 \(n \gg m\)이므로 시간 복잡도는 \(O(n)\)에 가까워진다.
  • 공간 복잡도 \(O(n + m)\), 제자리 정렬이 아님: 길이가 각각 \(n\)과 \(m\)인 배열 res와 counter를 사용한다.
  • 안정 정렬: res에는 '오른쪽에서 왼쪽으로' 원소를 채우므로, nums를 역순으로 순회하면 값이 같은 원소의 상대적 위치가 바뀌지 않아 안정 정렬이 된다. 정방향으로 순회해도 정렬 결과 자체는 올바르지만 안정적이지 않다.

11.9.4   한계

여기까지 보면 계수 정렬이 매우 기발하다고 느낄 수 있다. 개수만 세어도 효율적으로 정렬할 수 있기 때문이다. 하지만 계수 정렬을 사용하기 위한 전제 조건은 비교적 엄격하다.

계수 정렬은 음이 아닌 정수에만 적합하다. 다른 타입에 적용하려면 해당 데이터를 음이 아닌 정수로 바꿀 수 있어야 하며, 변환 과정에서 원소 간 대소 관계가 바뀌면 안 된다. 예를 들어 음수가 포함된 정수 배열은 모든 수에 일정한 상수를 더해 양수로 바꾼 뒤, 정렬 후 다시 원래 값으로 돌릴 수 있다.

계수 정렬은 데이터 수는 많지만 값의 범위는 작은 경우에 적합하다. 위 예시에서 \(m\)이 너무 크면 공간을 지나치게 많이 차지한다. 또한 \(n \ll m\)이면 계수 정렬에 \(O(m)\) 시간이 걸리므로 \(O(n \log n)\) 정렬 알고리즘보다 느릴 수도 있다.

댓글로 의견이나 질문, 제안을 남겨 주세요