- data compression : 데이터 파일을 encoding하는 효율적인 방법을 찾는 것. (데이터 압축)
- data transmission : 데이터 파일을 전송하는 효율적인 방법을 찾는 것. (데이터 전송)
- 허프만 코드 : 파일을 저장하기 위한 방법으로, 이진 코드(binary code)가 가장 많이 쓰인다.
- 이진 코드 : 파일의 각 문자를 소위 codeword라고 부르는 유일한 비트 스트링으로 대응시켜 표현한 것.
- fixed-length binary code (고정 길이 이진 코드)
각 문자를 같은 길이의 비트 스트링으로 표현한다. 예를 들자면, 2-bit 짜리 문자의 결합이 {a,b,c}일 때 이를 나타내면
a : 00 b : 01 c : 11
파일 ababcbbbc 를 코드화하여 나타낸다면
000100011101010111 (18 bits) 이 된다.
- variable-length binary code (가변 길이 이진 코드)
각 문자를 나타내는 비트 스트링의 길이가 다를 수 있다. (어떤 문자는 1비트, 다른 문자는 2비트 등)
a : 10 b : 0 c : 11
파일 ababcbbbc를 코드화하여 나타내면
1001001100011 (13bits)이 된다. 고정 길이를 사용했을 때보다 전체 필요한 비트 수가 줄어든다!
이와 같이, 파일이 주어졌을 때 기본 구성 요소인 각 문자를 어떤 이진 코드로 나타내는 것이
파일을 저장할 때 공간이 가장 적게 필요한지 찾아내는 문제를 최적 이진 코드 문제라고 한다.
이러한 최적 이진 코드 문제를 greedy algorithm으로 풀 수 있는 한 가지 방법이 허프만의 알고리즘이다.
허프만의 알고리즘에 대해 알아보기 전에, 먼저 prefix code에 대해 설명한다.
prefix code는 앞자리 부호, 즉 앞에 붙이는 한 자리 이상의 문자를 말한다.
만약, variable-length binary code가 다음과 같이 구성되어 있다고 하자.
a : 010 b : 1 c : 01 d : 11
이 때, 파일 bbabcbbbcd를 코드화해서 나타내면 110101011110111 이 된다.
하지만, 이 경우 이진 코드를 보고 파일을 복원한다고 했을 때,
다른 추가적인 정보 없이는 두 비트 11이 d인지, bb인지 혹은 중간에 나오는 0101이 cc인지 ab인지 구별할 수가 없다.
따라서, 이를 구별하기 위해서 어떤 문자의 이진 코드가 다른 문자의 이진 코드의 앞부분, 즉 prefix가 되어서는 안된다!
아래의 예시를 보면서 이를 이해해보자.
a : 010 b : 1 c : 00 d : 011
이진 코드를 살펴보면 b의 코드인 1은 a, c, d를 나타내는 prefix가 아니고, c의 코드인 00도 a, d의 prefix가 아니다.
따라서, 이러한 binary 코드는 prefix 코드의 성질을 만족한다.
이제 모호성이 없기 때문에 쉽게 복호화(decode)할 수 있을 것이다.
위와 같이, 각 문자를 복호화하는 과정을 나타내기 위해서 이진 트리를 이용하면 쉽게 표현될 수 있다.
- 각 단말 노드 : 그에 대응하는 문자
- 단말 노드의 숫자 : 대응되는 문자의 빈도수
- 내부 노드의 숫자 : 그 노드를 root로 하는 부분 트리에 있는 단말 노드들의 빈도수의 합
생성된 이진 트리는 다음과 같은 표를 제공한다.
a | b | c | d | e | f | |
fixed length codeword | 000 | 001 | 010 | 011 | 100 | 101 |
variable length codeword | 0 | 101 | 100 | 111 | 1101 | 1100 |
frequency (*1000) | 45 | 13 | 12 | 16 | 9 | 5 |
고정 길이 코드를 이용하면 6자를 표현하기 위해 3비트가 필요하다. 전체 frequency의 합은 100이기 때문에 3*100*1000=300000. 따라서, 전체 파일을 코딩하려면 300,000비트가 필요!
하지만, 가변 길이 코드를 사용한다면, 빈번한 문자에는 짧은 codeword를 부여하고, 빈번하지 않은 문자에는 긴 codeword를 부여하여 더 효율적으로 수행할 수 있을 것이다. 계산해보면, 45 X 1 + 13 X 3 + 12 X 3 + 16 X 3 + 9 X 4 + 5 X 4) X 1000 = 224,000 비트이고, 이는 무려 메모리의 25%를 절약할 수 있다!
주의 ! 이때, prefix code를 고려해서 구현한다. abc를 구현한다면 0.101.100 = 0101100 이런 식으로 연결을 하여 구현.
0101100을 디코딩할 때, a.b.c 이런 식으로 고유한 문자를 매칭할 수 있기 때문에 혼동이 없다.
각 문자에 대한 이진 코드는 root로부터 각 단말 노드까지 가는 경로에 의해 표현된다.
왼쪽 자식 노드로 가면 0, 오른쪽 자식 노드로 가면 1이고, 내부 노드는 항상 자식 노드가 2개이다.
이처럼 이진 트리 T를 생성하여 전체 비트 수를 최소화하는 욕심쟁이 알고리즘이 허프만 알고리즘이다.
0. 우선 빈도수가 적은 순서대로 정렬한 후 시작한다.
-
각 기호에 대해 리프 노드를 작성하여 우선 순위 큐에 추가한다.
-
대기열에 둘 이상의 노드가 있는 동안 :
-
대기열에서 가장 높은 우선 순위의 두 노드를 제거한다.
-
이 두 노드를 자식 노드로하고 두 노드의 빈도의 합과 같은 빈도로 새 내부 노드를 만든다.
-
새 노드를 큐에 추가함.
-
-
나머지 노드는 루트 노드이며 허프만 트리가 완성된다.
typedef struct node{
struct node *left; // left child
struct node *right; // right child
}node
*node huffman (int n,node PQ){
node *a, *b, *r;
int i;
for (i=1;i<n;i++){
remove(PQ,a);
remove(PQ,b);
r->left=a;
r->right=b;
r->freq=a->freq+b->freq;
insert(PQ,r);
}
remove(PQ,r);
return r;
}
코드 설명 : 우선순위 큐에서 노드를 두 개 제거하고, 다시 우선 순위 큐에 제거한 노드 두 개를 합친 노드 r을 삽입한다.
이 과정을 반복하여 나온 허프만 트리의 루트를 반환한다.
시간 복잡도
우선순위 큐로 heap이 사용된다면, 초기에 힙을 구성하는 데 O(n) 시간이 걸리고, 최솟값 제거 및 새로운 원소의 삽입 등 heap 연산은 O(logn) 시간이 걸린다. for 루프는 n-1번 수행되기 때문에 알고리즘의 전체 수행 시간은 O(nlogn)이 된다.
'Personal Study > Computer Algorithm' 카테고리의 다른 글
동적계획법 (Dynamic Programming) - (2) 최장 증가 부분 수열 (LIS) (0) | 2019.04.22 |
---|---|
동적 계획법 (Dynamic Programming) -(1) 이항계수 (0) | 2019.04.22 |
배낭 문제 (Fractional Knapsack and 0-1 Knapsack Problem) (0) | 2019.04.21 |
크루스칼 알고리즘 (Kruskal Algorithm)과 find-union 자료구조 (0) | 2019.04.21 |
다익스트라 알고리즘 (Dijkstra algorithm) (0) | 2019.04.07 |