알고리즘

Counting Sort : 계수 정렬

벨포트조던 2016. 12. 7.
반응형

http://bowbowbow.tistory.com/8



소개

Counting Sort는 정렬 알고리즘으로 Counting Sort : 계수 정렬 - 소개의 시간복잡도를 갖습니다. 반면 일반적 상황에서 가장 빠른 정렬 알고리즘인 Quick Sort의 시간복잡도 Counting Sort : 계수 정렬 - 소개입니다.

Counting Sort는 어떻게 이렇게 빠를까요? 그럼 왜 대부분의 정렬이 필요한 상황에서 더 빠른 Counting Sort를 안 쓰고 Quick Sort를 쓸까요?

Counting Sort가 어떻게 작동하는지 이해하고 나면 위 의문에 대한 답을 스스로 할 수 있을 겁니다!

정렬 과정

다음과 같은 수열 A를 정렬 해야하는 상황을 생각해봅시다.

Counting Sort : 계수 정렬 - 정렬 과정

위 수열을 정렬하면 아래와 같은 수열 B를 얻습니다.

Counting Sort : 계수 정렬 - 정렬 과정

Counting Sort가 어떻게 수열 A를 정렬해서 수열 B를 얻는지 따라가 봅시다.

  1. 첫번째로 각 숫자가 몇번 등장하는지 세어줍니다.
숫자012345
등장 횟수322333

벌써 Counting Sort의 이름에 왜 Counting이 들어가있는지 알것 같지 않나요? 이렇게 각 숫자가 몇번 등장했는지 세어주기 때문입니다.

cf) 이 예제만 보면 1번 과정만 하고 벌써 정렬을 다 한 것 같습니다. 각 숫자가 등장한 횟수만큼 작은 순서대로 찍어주면 되기 때문이죠. 그럼 왜 굳이 아래 과정을 할까요?

Counting Sort : 계수 정렬 - 정렬 과정 같은 수열을 정렬하는 상황을 생각해보세요. 중간에 등장한 뜬금없이 큰 100이 있어서 3~99까지 무의미한 순회를 해야하죠. 따라서 방금 생각한 방법은 숫자 크기에 시간복잡도가 매우 큰 영향을 받으므로 비효율적이라는 것을 알 수 있습니다.

  1. 등장 횟수를 누적합으로 바꿔줍니다.
숫자012345
누적 합3 (3)5 (3+2)7 (3+2+2)10 (3+2+2+3)13 (3+2+2+3+3)16 (3+2+2+3+3+3)

이 누적합에서 알 수 있는 것은 숫자 0은 1 ~ 3인덱스에 위치하고 숫자 2는 4 ~ 7 인덱스에 위치한다는 것입니다.

  1. 정렬할 배열 A를 뒤에서 앞으로 순회 하면서 정렬된 배열 B 에 넣어줍니다. 2번 과정에서 구한 누적합이 배열 A의 숫자가 배열 B의 어디에 위치해야 할지 정확하게 알려줍니다. 아래 5장의 이미지는 3번의 진행 과정을 설명합니다.


Counting Sort : 계수 정렬 - 정렬 과정

Counting Sort : 계수 정렬 - 정렬 과정

Counting Sort : 계수 정렬 - 정렬 과정

Counting Sort : 계수 정렬 - 정렬 과정

Counting Sort : 계수 정렬 - 정렬 과정



애니메이션 예시

예시 전체를 그림을 만드는건 힘들어서.. 누군가 멋지게 만들어주신 Counting Sort 애니메이션 링크를 첨부합니다. 아래 링크에서 Counting Sort 알고리즘 진행 과정 전체 예시를 볼 수 있습니다. 
http://www.cs.miami.edu/home/burt/learning/Csc517.091/workbook/countingsort.html

구현

원래 배열을 A, 정렬된 배열을 B, 각 숫자가 몇개인지 세어준 배열을 count, 숫자 개수의 누적합을 담은 배열을 countSum이라 이름지었습니다.


#include <cstdio>
#define MAX_SIZE 1000
#define MAX_NUM 10000
int main(){
int N, A[MAX_SIZE+1], B[MAX_SIZE+1], count[MAX_NUM+1], countSum[MAX_NUM+1];
//수열의 길이 N은 MAX_SIZE이하여야합니다.
scanf("%d", &N);
//count배열을 모두 0으로 초기화
for(int i = 0; i<= N ; i++)
count[i] = 0;
//수열 A에 입력되는 수는 MAX_NUM 이하여야합니다.
for(int i =1 ; i<= N ; i++){
scanf("%d", &A[i]);
//숫자 등장 횟수 세기
count[A[i]]++;
}
//누적합 구성
countSum[0] = count[0];
for(int i = 1 ; i<= MAX_NUM ; i++)
countSum[i] = countSum[i-1]+count[i];
//뒤에서 부터 수열 A 순회한다.
for(int i = N ; i >= 1; i--){
B[countSum[A[i]]] = A[i];
countSum[A[i]]--;
}
//수열 A를 정렬한 결과인 수열 B를 출력한다.
for(int i =1 ; i<= N ; i++)
printf("%d ", B[i]);
}
view rawcounting_sort.cpp hosted with ❤ by GitHub

아래 코드는 파이썬 버전입니다.

#-*- coding: utf-8 -*-
#한글 주석을 위해 인코딩 형식 utf-8을 선언함
#counting sort python3 version
#python3 문법으로 작성되었습니다.
#입력 예시 : 1 3 17 5 7
#출력 예시 : 1 3 5 7 17
#입력될 수 있는 숫자의 최대 크기를 의미합니다.
MAX_NUM = 1000
#A는 입력된 숫자를 저장하는 배열
A = list(map(int, input().split()))
#N은 입력된 숫자의 개수 입니다.
N = len(A)
#0으로 초기화된 입력될 MAX_NUM+1 길이의 배열 count를 생성합니다.
count =[0]*(MAX_NUM+1)
#countSum은 누적합을 저장하는 배열입니다.
countSum =[0]*(MAX_NUM+1)
#숫자 등장 횟수 세기
for i in range(0, N):
count[A[i]] += 1
#숫자 등장 횟수 누적합 구하기
countSum[0] = count[0]
for i in range(1, MAX_NUM+1):
countSum[i] = countSum[i-1]+count[i];
#B는 정렬된 결과를 저장하는 배열
B = [0]*(N+1)
for i in range(N-1, -1, -1):
B[countSum[A[i]]] = A[i]
countSum[A[i]] -= 1
#수열 A를 정렬한 결과인 수열 B를 출력한다.
result = ""
for i in range(1, N+1):
result += str(B[i]) + " "
print(result)
view rawconting_sort.py hosted with ❤ by GitHub


정리

알고리즘의 진행과정을 따라가보면서 처음 소개에서 했던 의문들에 대한 답을 얻을 수 있었을 겁니다.

Counting Sort 알고리즘의 시간복잡도는 O(n) 으로 Quick Sort보다 훨씬 유리해보입니다. 그러나 세상에 공짜는 없다는 말처럼 Counting Sort는 대부분의 상황에서 엄청난 메모리 낭비를 야기할 수 있습니다.

누적합 배열에 대한 접근을 O(1)에 달성하기 위해 정렬할 배열에 포함된 숫자의 최댓값 만큼의 메모리를 필요로 합니다. 아까 추가로 예시든 Counting Sort : 계수 정렬 - 정리 배열에 Counting Sort 알고리즘으로 정렬하기 위해서는 누적합 배열의 길이를 100으로 잡는 낭비를 해야합니다. 만약 배열에 최댓값으로 10억이 포함되어 있다면 엄청난 낭비가 되겠죠.

따라서 Counting Sort는 위에서든 예시처럼 

Counting Sort : 계수 정렬 - 정리
정렬하는 숫자가 특정한 범위(위 예시 : 0~5) 안에 있을 때 사용하게 됩니다.

Counting Sort를 대표적으로 활용하는 사례는 26개의 알파벳으로 이루어진 문자열에서 Suffix Array를 얻는 경우인데 이때 Counting Sort를 사용하기 때문에 일반적인 Sort를 사용해서 Suffix Array를 얻때 시간복잡도 Counting Sort : 계수 정렬 - 정리보다 빠른 Counting Sort : 계수 정렬 - 정리에 Suffix Array를 얻는 것이 가능합니다.


반응형

댓글