May 31, 2020
모든 정렬 알고리즘은 기본적으로 배열의 요소들을 검사하는 과정이 포함되어 있다. 결국 배열의 데이터들을 비교하기 위해서는 Decision Tree 를 만들어 경우의 수를 따져볼 수 있는데, 이로부터 우리는 decision tree 의 높이인 logn 만큼 비교연산이 일어난다는 것을 알아낼 수 있다. 비교연산의 시간 복잡도는 Օ(n) 이므로 비교연산을 사용하는 정렬 알고리즘은 아무리 빨라도 Օ(nlogn)보다 빠를 수가 없다.
우리가 자주 언급하는 빠른 정렬알고리즘인 퀵소트, 머지소트 역시 Օ(nlogn)을 복잡도로 가진다.
그렇다면 만약 비교연산을 사용하지 않는다면 어떨까? 비교연산을 사용하지 않는다면 Decision Tree 의 제약사항 없이 더 빠른 정렬이 가능하지 않을까?
계수정렬 혹은 카운팅 소트는 이런 아이디어를 기반으로 해서 나온 Օ(n+데이터의 최대값 k)의 속도가 보장되는 정렬 알고리즘이다. 카운팅 소트는 이름 그대로 배열 내에 특정한 값이 몇번 등장했는지에 따라 정렬을 수행하기 때문에 비교연산이 사용되지 않는다.
카운팅 정렬은 다음과 같은 과정으로 수행된다.
(B[A[i]]++)B[i] = B[i] + B[i-1]C[B[A[i]]] = A[i](B[A[i]]--)배열이 여러개 사용되고 서로가 서로를 인덱스로 사용하는 상황이랑 글로는 이해하기가 쉽지 않다. 그림으로 알아보자.
일단 배열 A를 입력받았고, 배열 B와 C를 준비했다.
제일 먼저, A를 순회하면서 A에 들어가 있는 값들의 등장 횟수를 배열 B에 저장해주었다. 예를 들어 A에서 0이라는 값은 존재하지 않기 때문에 B의 0번 인덱스는 0이되고, 3은 총 2번 등장했기 때문에 B의 3번 인덱스는 2로 걍신되었다.
주목할 점은 B의 배열길이는 A에 있는 최댓값에 따라 결정된다는 것이다. 이후에 이것이 성능에 어떤 영향이 있을지 논의해보자.
B 배열의 값들을 1부터 마지막 값까지 바로 이전 값과 계속 더해서 누적합으로 만들어준다.
정렬 끝!
// k == max number
// n == number of data in A
void counting_sort(int A[], int B[], int C[]){
/* 카운팅 배열 0으로 초기화 */
for (int i = 0 ; i <= k ; i++){
B[i] = 0;
}
/* 카운팅 값 갱신 */
for (int i = 1 ; i <= n ; i++){
B[A[i]] = B[A[i]] + 1;
}
/* 누적합 계산 */
for (int i = 1 ; i <= k ; i++){
B[i] = B[i] + B[i-1];
}
/* 결과 배열에 값 넣기 */
for (int i = n ; i >= n ; i--){
C[B[A[i]]] = A[i];
B[A[i]] = B[A[i]] - 1;
}
}앞서 잠깐 언급했던 것 처럼 이 알고리즘의 치명적인 단점은 카운팅을 위한 배열의 길이가 입력받은 배열 값들 중 최댓값에 의해 결정된다는 것이다. 만약 우리가 입력받은 배열의 값들이 (1, 100000) 이라고 한다면, 이 배열은 두 개의 값만을 가지고 있음에도 불구하고 카운팅 배열의 크기는 100000으로 설정되어야 한다.
또한 이 알고리즘의 시간 복잡도는 Օ(n * k) 가 되는데, 배열을 참조하는 연산 O(n) 이 B 배열의 길이인 k, 즉 배열 내의 최대 값의 길이만큼 반복되기 때문이다. 따라서 입력받은 배열의 데이터개수가 k 보다 작다면 굉장히 유용한 알고리즘이 되겠지만, k 가 더 큰경우에는 비효율적인 알고리즘이 될 수도 있다.