Counting Sort는 조금은 특이한 정렬 알고리즘이다.

정렬 알고리즘 중 유일하게 O(N)의 시간복잡도를 가지고 있다.

(정확히는 O(N+E) 이다.   N : 인풋 데이터 수,  E : 주어지는 데이터의 범위 ( 보통 0 ~ 최대값 ) )

따라서 코딩테스트에서 정렬 API로 정렬하지 못하는 데이터를 시간 내에 정렬할 수도 있음으로 알아두면 좋다! 

또한 카운팅 정렬을 단순하게 구현하면 불안정 정렬로 구현될 수 있다.

안정 정렬로 구현하는 것이 바람직함으로 두 가지 방법 모두 알아보자!

 

1. 불안정 정렬 구현

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] nums = {0,4,1,4,1,2,4,1};
        int N = nums.length;
        int K = 4; // 최대값
        int[] count = new int[K+1];

				// 카운팅
        for (int i = 0; i < N; i++) {
            count[nums[i]]++;
        }

				// 정렬
        int idx = 0;
        for (int i = 0; i <= K; i++) {
            for (int j = 0; j < count[i]; j++) {
                nums[idx+j] = i;
            }
            idx += count[i];
        }

        System.out.println(Arrays.toString(nums));

    }
}
[0,1,1,1,2,4,4,4]

 

다만 불안정 정렬로 1번 idx에 있는 1이 2번 idx에 있는 1보다 더 뒤에 있던 값이 아닐 수 있다.

해당 문제는 2차원 배열이나 Class 변수와 같이 파라미터가 2개 이상인 데이터의 정렬에서 문제가 생길 수 있음으로 안정 정렬로 이를 개선하는 방법을 고민해보자.

 

 

2. 안정 정렬

그러면 어떻게 안정 정렬로 개선할 수 있을까?

누적합과 역순 탐색을 이용하면 되는데 카운팅 배열을 누적합하게 되면 ( 누적합 배열에 저장된 값- 1 ) 이 해당 idx의 마지막 값이 들어갈 위치이다! 

 

예시로 살펴보자

주어진 배열은 아래와 같다

1 3 3 2 4 1 4 3

이를 카운팅 하고 누적합한 결과는 아래와 같다.

카운팅 배열 idx 1 2 3 4
카운팅 2 1 3 2
누적합 2 3 (2+1) 6 (2+1+3) 8

정렬 결과는 아래와 같다

idx 0 1 2 3 4 5 6 7
정렬 결과 1 1 2 3 3 3 4 4

정렬 결과에서 정렬 결과가 바뀌는 지점, 즉 어떠한 수가 마지막으로 나오는 위치를 자세히 보자.

앞에서 부터 순서대로 보면

1이 2개 있음으로 idx 1까지,

2는 1개 있음으로 1의 마지막 idx인 1에서 한 칸 뒤인 idx 2까지,

3은 3개 있음으로 2의 마지막 idx인 2에서 세 칸 뒤인 idx 5까지인 것을 알 수 있다. (4도 마찬가지이다)

이 결과를 보고 누적합 결과를 보면 누적합 값에서 1을 빼면 현재 숫자가 마지막으로 나오는 위치를 알려준다고 할 수 있다.

 

그러면 이를 어떻게 안정정렬에 확인할 수 있는가?

안정정렬은 만약 비교하는 파라미터의 값이 같다면 그 데이터는 기존 순서를 보장해야 한다.

따라서 같은 3이라도 원래 맨 뒤에 위치했던 3은 맨 뒤에 있어야 한다.

그런데.. 우리는 누적합 배열을 통해 어떠한 수가 마지막으로 나오는 위치를 알 수 있게 되었다!

따라서 원 배열을 뒤에서부터 탐색하여 해당 수의 마지막 위치에 넣어주면 되지 않을까?

 

 

다시 원 배열을 살펴보자

역순으로 탐색하기로 했으니, 원 배열의 마지막 값은 3이고, 3은 총 3개가 있다.

1 3 (1) 3 (2) 2 4 1 4 3 (3)

 

우리가 보고 있는 마지막 3의 누적합 값은 6이다.

그 뜻은, 정렬 되었을 때 6-1 인 5번 idx 위치가 3의 마지막 위치라는 것이다

카운팅 배열 idx 1 2 3 4
카운팅 2 1 3 2
누적합 2 6 8

따라서 해당 위치에 정렬할 수 있다.

idx 0 1 2 3 4 5 6 7
정렬 결과 1 1 2 3 3 3 (3) 4 4

마지막으로 3이 1개 정렬되었음으로 누적합 값을 1 감소시켜준다.

그러면 다음 3이 나온다면 해당 3은 4번 idx에 위치시키면 된다는 것을 알게 될 것이다!

카운팅 배열 idx 1 2 3 4
카운팅 2 1 3 2
누적합 2 5 8

 

 

한 번만 더 보자! 다음 값은 4이다.

1 3 3 2 4 (1) 1 4 (2) 3

4의 누적합 값은 8임으로 정렬된 배열의 7번 idx에 마지막 4가 위치한다는 뜻이다.

카운팅 배열 idx 1 2 3 4
카운팅 2 1 3 2
누적합 2 5 8

따라서 해당 위치에 정렬할 수 있다.

idx 0 1 2 3 4 5 6 7
정렬 결과 1 1 2 3 3 3 4 4 (2)

마지막으로 4가 1개 정렬되었음으로 누적합 값을 1 감소시켜준다.

카운팅 배열 idx 1 2 3 4
카운팅 2 1 3 2
누적합 2 5 7

이 로직을 반복하면 안정정렬이 가능하다!

 

위 로직을 코드로 살펴보자

import java.util.Arrays;

public class CountingSort {
    public static void main(String[] args) {
        int[][] nums = {{0,1}, {1,4}, {4,3}, {5,2}, {2,4}, {3,3}, {3,2}, {3,1}, {1,5}};

        int N = nums.length;

        // 최대값을 구한다.
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < N; i++) {
            if (nums[i][0] > max) max = nums[i][0];
        }
        // 카운팅 배열을 만든다.
        int[] count = new int[max + 1];
        for (int i = 0; i < N; i++) {
            count[nums[i][0]]++;
        }

        // 카운팅 배열을 누적합한다.
        for (int i = 1; i <= max; i++) {
            count[i] += count[i-1];
        }

        // 누적합 값의 위치는 해당 인덱스의 값을 가진 맨 뒤에 위치했던 데이터가 들어가는 위치보다 1칸 뒤의 값이다 
        // (예를 들어 index 2의 누적합이 6라면 실제 배열에서 index 5 위치에 마지막 2짜리 값이 들어갈 수 있다)
        int[][] result = new int[N][2];
        for (int i = N-1; i >= 0; i--) {
            int n = nums[i][0];
            result[--count[n]] = nums[i];
        }

        System.out.println(Arrays.deepToString(result));
    }


}
[[0, 1], [1, 4], [1, 5], [2, 4], [3, 3], [3, 2], [3, 1], [4, 3], [5, 2]]

 

3. 최소값 고려하기

카운팅 정렬에는 치명적인 단점이 있는데

최대값과 최소값의 범위가 크면 시간복잡도 (O(N+E) , E : 최대~최소값 범위) ,

공간복잡도 (최대값 + 1 크기의 카운팅 배열 필요) 가 오히려 높게 올라갈 수 있다.

 

만약 문제에서 최대값이 크나 최소값도 커서 주어진 값의 범위 자체는 좁은 경우에는

카운팅 배열의 범위를 좁히는 방법으로 개선해 볼 수 있다.

 

아래 코드 예시를 보자. 

2번 코드에서 주어지는 인풋값이 모두 1000씩 증가했음으로 6개 짜리 int 배열에서 1006개 짜리 int 배열이 필요했으나

최소값을 같이 고려하면 똑같이 6개짜리 int 배열로 정렬할 수 있다.

어려운 내용이 아니니 코드만 살짝 봐보자

 

import java.util.Arrays;

public class CountingSort {
    public static void main(String[] args) {
        int[][] nums = {{1000,1001}, {1001,1004}, {1004,1003}, {1005,1002}, {1002,1004}, {1003,1003}, {1003,1002}, {1003,1001}, {1001,1005}};

        int N = nums.length;

        // 최대값 & 최소값을 구한다.
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < N; i++) {
            if (nums[i][0] > max) max = nums[i][0];
            if (nums[i][0] < min) min = nums[i][0];
        }
        // 카운팅 배열을 만든다. 최소값이 0에 가도록 idx를 조정해준다
        int[] count = new int[max - min +1];
        for (int i = 0; i < N; i++) {
            count[nums[i][0] - min]++;
        }

        for (int i = 1; i <= max-min; i++) {
            count[i] += count[i-1];
        }

        int[][] result = new int[N][2];
        for (int i = N-1; i >= 0; i--) {
            int n = nums[i][0] - min;
            result[--count[n]] = nums[i];
        }

        System.out.println(Arrays.deepToString(result));
    }


}
[[1000, 1001], [1001, 1004], [1001, 1005], [1002, 1004], [1003, 1003], [1003, 1002], [1003, 1001], [1004, 1003], [1005, 1002]]

+ Recent posts