2007년 05월 17일
데이터 압축에 쓰인다는(?) 허프만 코드... Huffman code
이번 것은 과제는 아니였는데, 오기로(?) 해 버렸다. 크루스칼 알고리즘을 힘들게 해결한 이후에, 이제는 슬슬 인터넷에서 소스를 찾기보다는 내 자신이 교재(참고자료 참고)의 pseudo code(의사 코드)를 보고, 몇시간을 감이 잡힐 때까지 그림을 그려보거나 골똘히 생각하다가 조금씩 실마리가 잡히면 코딩을 시작하는 형태가 되어 가고 있다.
그리고 지금 보고 있는 교재의 pseudo code가 다른 알고리즘이나 자료구조 책보다 깔끔하게 정리가 잘 되어 있다는 생각이 든다. 필요한 부분만 쉽게 설명해주고 pseudo code로 보여준다는 느낌이다.
허프만 코드는 기존의 영문자 ASCII 코드가 7비트(8비트)로 문자를 표현하고 있지만, 실재로는 하나의 문서에서 몇백개나 되는 문자를 모두 쓰는 경우는 매우 드문다는 점에 착안해서, 쓰는 문자만 뽑아내고 그 문자에 가중치(빈도수)를 주어서, 자주 나오는 문자를 짧게 부호화(이진코드화)해서, 하나의 문자로 봤을때나 전체의 문자열로 봤을 때나, ASCII 코드보다 적은 비트로 표현이 가능해져서 압축을 하는 효과가 된다는 개념이다.
지금 학교에서 배우는 교재(참고자료 참고)에서는 이번 알고리즘 역시 탐욕적인 방법으로 분류되어 나온다. 지금까지 탐욕적인 알고리즘을 몇가지 봤는데, 분할정복법의 요점이 재귀, 동적계획법의 요점이 행렬(배열)이였다면 탐욕적인 알고리즘의 요점은 정렬이라고 느꼈다. 크루스칼이나 프림의 알고리즘도 그 순간에 최적인 방법을 택하기 위해서는 정렬이 필수였고, 지금 보는 허프만 코드 역시 이진 트리를 구성하기 위해서는 정렬된 우선순위 대기열을 사용한다.
허프만 코드에서 이진 트리를 어떻게 만드는지와 같은 알고리즘이 돌아가는 방식에 대해서는 인터넷에서 검색하면 많이 나오므로 생략한다. 소스는 철절히 교재의 pseudo code의 리듬으로 짰으므로, 교재가 있다면 이해하는데 어렵지 않으리라 생각된다.
이건 개인적인 얘기이고 지난번에도 얘기 했지만, 알고리즘 관련 포스팅의 경우 조회수는 참 높은데, 조회수에 비해 댓글은 거의 없다. 뭔가 댓글을 좀 달아주셨으면 좋겠다. 이 부분은 이렇게 하는게 더 좋다라든지... 소스의 어느 부분이 문제라든지.. 아니면 잘 가져가서 쓴다라는 댓글이라도 좋다. 자기 블로그(홈페이지)가 있다면 주소도 좀 남겨주셨으면 좋겠다. 아니면 리퍼러에 남도록 방문해 주면 알아서 찾아갈터인데...
허프만 코드 알고리즘에서 쓰일 텍스트 파일 파일(huffman.txt)
결과는 다음과 같다.
(참고로 나는 프로그램을 잘하거나 소질이 있는 것이 아니라서 이 정도의 프로그램을 짜려면 며칠이 걸린다. 이 허프만 코드의 경우 코딩을 시작 하기까지 두시간 정도를 책을 보면서 어떻게 구현할 수 있을까 생각한 후에, 코딩을 시작했고 꼬박 앉은 자리에서 열시간 정도를 삽질을 하면서 프로그램을 트리 구성까지 완성시켰다. 그리고 마지막 결과 트리 출력 부분을 만들기 위해 추가로 서너시간 가량이 걸렸다.)이렇게 최근 알고리즘에 열을 올리면서 이전에는 왜 알고리즘이나 자료구조 책에는 바로 컴파일을 할 수 있는 소스가 아니고, pseudo code만 나와있어서 이렇게 사람을 힘들게 하냐고 생각 했는데... 최근 1~2주 사이에는 힘들어도 pseudo code를 보면서 내 입맛에 맞춰서 코딩(요리)을 하는 재미도 나름대로 솔솔하다는 생각도 든다.
그리고 지금 보고 있는 교재의 pseudo code가 다른 알고리즘이나 자료구조 책보다 깔끔하게 정리가 잘 되어 있다는 생각이 든다. 필요한 부분만 쉽게 설명해주고 pseudo code로 보여준다는 느낌이다.
허프만 코드는 기존의 영문자 ASCII 코드가 7비트(8비트)로 문자를 표현하고 있지만, 실재로는 하나의 문서에서 몇백개나 되는 문자를 모두 쓰는 경우는 매우 드문다는 점에 착안해서, 쓰는 문자만 뽑아내고 그 문자에 가중치(빈도수)를 주어서, 자주 나오는 문자를 짧게 부호화(이진코드화)해서, 하나의 문자로 봤을때나 전체의 문자열로 봤을 때나, ASCII 코드보다 적은 비트로 표현이 가능해져서 압축을 하는 효과가 된다는 개념이다.
지금 학교에서 배우는 교재(참고자료 참고)에서는 이번 알고리즘 역시 탐욕적인 방법으로 분류되어 나온다. 지금까지 탐욕적인 알고리즘을 몇가지 봤는데, 분할정복법의 요점이 재귀, 동적계획법의 요점이 행렬(배열)이였다면 탐욕적인 알고리즘의 요점은 정렬이라고 느꼈다. 크루스칼이나 프림의 알고리즘도 그 순간에 최적인 방법을 택하기 위해서는 정렬이 필수였고, 지금 보는 허프만 코드 역시 이진 트리를 구성하기 위해서는 정렬된 우선순위 대기열을 사용한다.
허프만 코드에서 이진 트리를 어떻게 만드는지와 같은 알고리즘이 돌아가는 방식에 대해서는 인터넷에서 검색하면 많이 나오므로 생략한다. 소스는 철절히 교재의 pseudo code의 리듬으로 짰으므로, 교재가 있다면 이해하는데 어렵지 않으리라 생각된다.
이건 개인적인 얘기이고 지난번에도 얘기 했지만, 알고리즘 관련 포스팅의 경우 조회수는 참 높은데, 조회수에 비해 댓글은 거의 없다. 뭔가 댓글을 좀 달아주셨으면 좋겠다. 이 부분은 이렇게 하는게 더 좋다라든지... 소스의 어느 부분이 문제라든지.. 아니면 잘 가져가서 쓴다라는 댓글이라도 좋다. 자기 블로그(홈페이지)가 있다면 주소도 좀 남겨주셨으면 좋겠다. 아니면 리퍼러에 남도록 방문해 주면 알아서 찾아갈터인데...
허프만 코드 알고리즘에서 쓰일 텍스트 파일 파일(huffman.txt)
b:5 e:10 c:12 a:16 d:17 f:25주어진 문자와 가중치로 트리를 구성해서 허프만 부호를 뽑아내는 허프만 코드 알고리즘
#include <stdio.h>
#include <stdlib.h>
typedef struct nodetype
{
char symbol; // 문자값
int frequency; // 파일에 있는 문자의 빈도수
struct nodetype* left;
struct nodetype* right;
}node;
typedef struct pq // 우선순위 대기열
{
node* Node; // 대기열에 있는 node
struct pq* next; // 다음 node
}pq;
#define ROOT -1 // 부호화 함수 출력시 초기값
pq* insert(node* r); // 우선순위 대기열에 노드를 정렬하여 삽입
node* huffman(int n); // 허프만 코드 트리를 만드는 함수
node* Remove(); // 우선 순위 대기열에서 노드를 삭제
void print_PQ(); // 우선 순위 대기열 출력
void print_tree(node* r, int n, char* code); // 결과 허프만 부호화 출력
void freetree(node* r); // 메모리 해제
pq* PQ = NULL; // 우선순위 대기열
FILE* file; // 파일 식별자
int main()
{
int n = 1;
node* result = (node*) malloc(sizeof(node)); // 결과를 돌려받을 노드
char* code = (char*) malloc(sizeof(char));
file = fopen("huffman.txt", "r"); // 입력이 되는 문자와 빈도수
if(file == NULL) {
perror("File open error");
return 0;
}
while(1) {
node* r = (node*) malloc(sizeof(node));
if( ( fscanf(file, "%c:%d ", &r->symbol, &r->frequency)) == -1) {
break; // 파일에 더 이상 값이 없으면 while 끝내기
}
r->left = NULL;
r->right = NULL;
insert(r); // 노드를 삽입 하면서 정렬함
n++;
}
printf("### Huffman Algorithm ###\n\n");
print_PQ(); // 입력이 끝난 우선순위 대기열
result = huffman(n); // 허프만 알고리즘
printf("\n-- Result Huffman code tree\n");
print_tree(result, ROOT, code);
printf("\n");
freetree(result); // 메모리 해제
fclose(file);
return 0;
}
void freetree(node* r) // 메모리 해제
{
if(r) {
freetree(r->left);
freetree(r->right);
free(r);
}
}
pq* insert(node* r) // 우선순위 대기열에 노드 삽입
{
pq* tmp = NULL;
pq* new = NULL;
// 우선순위 대기열 추가할 새로운 공간 할당
new = (pq*) malloc(sizeof(pq));
new->Node = r; // 대기열에 추가할 새로운 공간에 r 노드값 삽입
new->next = NULL;
if(PQ == NULL){ // 대기열이 비여있으면 새로운 공간이 리스트의 head가 됌
PQ = new;
return PQ;
} else if(PQ->Node->frequency > new->Node->frequency) {
new->next = PQ; // 루트 노드에 대한 비교 및 정렬
PQ = new;
}else { // 대기열에 리스트가 들어 있다면..
tmp = PQ; // tmp는 head
while(tmp->next != NULL) { // 다음 노드가 없을때까지..
if(tmp->next->Node->frequency <= new->Node->frequency)
tmp = tmp->next; // 우선순위 대기열 정렬을 위한 비교
else { // new의 빈도수가 tmp 보다 크고,
new->next = tmp->next; // tmp->next의 빈도수보다 작으면..
tmp->next = new; // 두 노드 사이에 새 노드를 삽입
break;
}
}
if(tmp->next == NULL) // 다음 대기열이 비어 있으면 삽입
tmp->next = new;
return tmp;
}
return 0;
}
node* huffman(int n) // 허프만 알고리즘
{
node* p;
node* q;
node* r;
int i;
for(i = 1; i < n-1; i++) {
p = Remove(); // 우선 순위 대기열에서 하나의 노드를 빼와서 p에 삽입
q = Remove(); // 우선 순위 대기열에서 하나의 노드를 빼와서 q에 삽입
r = (node*) malloc(sizeof(node)); // 새로운 부분 트리를 위한 공간 할당
r->left = p;
r->right = q;
r->frequency = p->frequency + q->frequency;
r->symbol = '*'; // 부분 트리의 뿌리임을 나타냄
insert(r); // 만들어진 부분 트리를 우선 순위 대기열에 삽입
printf("-- Create sub tree (%d/%d)\n ", i, n-2);
printf("%c:%d = %c:%d + %c:%d\n",
r->symbol, r->frequency, p->symbol, p->frequency, q->symbol, q->frequency);
print_PQ();
}
r = Remove(); // 우선 순위 대기열에 있는 트리의 뿌리를 빼와서 r에 대입
return r;
}
node* Remove() // 우선 순위 대기열에서 하나의 노드를 빼서 리턴값으로 넘긴다
{
node* pq_firstnode = NULL;
pq_firstnode = PQ->Node;
PQ = PQ->next;
return pq_firstnode;
}
void print_PQ() // 우선순위 대기열 출력
{
pq* tmp = PQ;
printf("-- Priority Queue\n | ");
while(tmp != NULL){
printf("%c:%d | ", tmp->Node->symbol, tmp->Node->frequency);
tmp = tmp->next;
}
printf("\n\n");
}
void print_tree(node* r, int n, char* code) // 결과 허프만 알고리즘 트리 출력
{
if(r) {
n++; // 트리의 깊이를 표시
code[n] = '0'; // 트리의 좌측
print_tree(r->left, n, code);
code[n] = '1'; // 트리의 우측
print_tree(r->right, n, code);
code[n] = '\0'; // 찌꺼기 제거
if(r->left == NULL || r->right == NULL) // 자식이 있는 노드는 출력하지 않음
printf(" - %c:%d\t= %s\n", r->symbol, r->frequency, code);
}
}
결과는 다음과 같다.
생각보다 마지막에 결과인, 허프만 코드(부호)를 출력하는데 무지하게 애를 먹었다. 어떻게 트리를 순회하면서, 노드의 값이 아닌, 노드와 노드사이를 연결한 방향을 나타내는 부호인 0과 1을 출력할 수 있을까? 하고...
- 참고자료
- 알고리즘(Foundatations of Algorithms Using C++ Pseudocode) - Neapolitan, Naimipour 저 / 도경구 역
# by | 2007/05/17 02:39 | :: R space :: 과제 | 트랙백 | 핑백(1) | 덧글(75)
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
... (Programmer's Moe Life)알고리즘 (sakuragi's miscellaneous space)가장 많이 읽힌 글은 데이터 압축에 쓰인다는(?) 허프만 코드... Huffman code (10월부터 집계)가장 적게 읽힌 글은 명작은 아니더라도 이정도면 수작... 캐러비안의 해적 3 - At World's End&nb ... more
전에 플로이드 알고리즘에 대해 올리셨는데 그 알고리즘으로 지하철
최소경로 프로그램을 짜려니 맵데이터가 난감하더군요...
더 효과적인게 없을까 같이 생각해 보아요~
그래도 알고리즘은 재밌는것 같다는 :)
ps. 터미널 글꼴이 무엇인지 물어봐도 될까요? ;)
하지만 문제는 플로이드 알고리즘을 쓰려면 행렬 데이터를 써야 하니, 이것도 문제네요. :(
비효율적이고, 난감해도 배열에 넣는 것 말고는 딱히 방법이 떠오르지 않네요. :)
환상경, 전 그저 평범해요.. :)
nidev, 네.. 저도 잘하지 못하지만 재미있는 점도 많은 것 같아요. :D
터미널 글꼴은 반달체(Bandal) 입니다. 크기는 9이구요.
방문자, 네~ 출처만 밝혀주세요~ :D
몬까 꼬이는 느낌 ~_~
왜 코드값출력할때 메모리 에러가 생기죠???
아공~~머리아포~~
MS Visual Stdio에서도 정상적으로 동작하도록 소스 코드 수정했습니다.
레포트가 허프만트리인데
참고하겠습니다
소스 코드를 직접 보고 참조하는 건 미친 짓이라는 생각도 듭니다만..
시간도 시간인데다..
시험이 더 중한 마당이라..
끝까지 정독 후 참조해서 리포트를 작성할까 생각이 드네요...;ㅁ;
여하튼.. 죄송합니다만..
리포트 쓰는 데 참조좀 하겠습니다..
(왠지 죄 짓는 기분..;ㅁ;)
이렇게 올린 이유 중에는 많은 분들께 도움이 되었으면 하는 마음도 있으니까요 :)
일단 AJ님께 조언하자면, 지하철 최단경로 알고리즘은 Floyd보다는 Dijkstra를 이용하는 편이 더 쉬울 거라고 봅니다. 이건 데이터가 반드시 인접행렬로 주어져야 하지는 않거든요. Floyd도 인접행렬 안 사용하고 할 수는 있지만 -,.-; 복잡하고, 지하철 경로 계산 프로그램에선 모든 쌍에 대해서 최단경로를 구해야 할 까닭이 없거든요.
참고자료에 적혀 있는 책의 내용과 그 책의 pseudo code를 바탕으로 짠 것이기 때문에 참고자료에 있는 책을 보시면 이해가 쉬우실 껍니다. 아마 도서관에 가면 있으리라 예상됩니다.
소스를 보시다가 어떤 부분이 잘 이해가 되지 않는다던지 하다면 제가 그 부분에 대해서 설명을 해 드릴 용의는 있으나, 소스를 첨부터 끝까지 분석해달라고 하시는 거면 그렇게는 못하겠네요. :)
위에 소스를 직접 쳐 가면서 파악을 해봤는데 역시 초보다 보니 이해가 되는 부분도 있고 안되는 부분도 있고 그렇네요. 근데 비쥬얼 C++ 에서 컴파일 시켜보니 에러가 뜨더군요;;
메시지를 살펴보면
파일경로huffman.cpp(157) : fatal error c1010 : unexpected end of file while looking for precompiled header directive Error executing cl.exe.
huffman.obj -1 error(s), 0 warning(s)
이유가 뭘까요? 첨에 프로젝트 설정할때 win32 colsol application 하고 simpleAppliction 선택을 했거든요 답변 부탁드릴께요
Win32 Console Application -> An empty project 를 선택하신 후, 새롭게 huffman.c와 같은 C 소스 파일을 만들어서 위의 허프만 코드 소스를 그대로 넣으면 에러 없이 컴파일이 되실 껍니다.
b:5 e:10 c:12 a:16 d:17 f:25
허프만 알고리즘에 대한 과제가 있었는데 .. 감사 합니다 ㅜ.ㅠ 혼자서 노력해서 과제를 하여야 하나 .
아직 실력이 .. ㅜ.ㅠ 님 덕분에 .. 좋은 자료 .. 좀 업어 가도 되겠지요 ㅡㅡ? 안된다면 흑흑흑..
위에 언급 하신 책도 지금 도서관에 빌리러 간답니다 ..
아무튼 감사 합니다 ..
좋은 자료라고 생각되시면 언젠가는 더 좋은 자료를 만들어서 다른 분들께 도움을 주세요~ :)
그리고 Microsoft Visual C++에서는 에러가 왕창 뜨는데,,,,
변수 이름 중에 new가 있더군요..... 에러는 new가 키워드라서 그렇습니다....
코드에서 new를 다른 변수명으로 바꿔 주시면 잘 돌아갑니다.... ^*^
마지막으로 sakuraki 님께 감사의 말씀 한번더 올립니다...... ㅎㅎ
1년 전에 포스팅 된 것인데도 정말 핵심만 잘 꼽아서 정리 해주셨어요.
다시 한 번 머리 숙여 감사~...
부족한 글에 칭찬해주셔서 감사합니다. ( __)
Visual Studio를 쓰지 않아서 정확한 답변은 못 드리겠네요.
출력문만 퍼갈게요.
저도 요즘 데이터 압축에 관심이 많은뎅
저는 VHDL로 짜봐야겠네요 ^^;;;
기억이 가물가물 해지려고 하네요~ ㅜ_ㅜ
궁금한게 있는데요...각 문자마다 코드가 출력되는데 이것을 붙여서 보여줄 순 없을까요...?
예를들면... txt파일에 'adf' 라고 입력하여 저장한 후 실행하면 '000110'가 나올 수 있게요...
void print_tree(node* r, int n, char* code) 함수의
printf에서 code만 출력하도록 수정하면 됩니다.
decoding하는 과정에서 어떻게해야댈지 막막하네요...
혹시 decoding하는 과정도 좀 설명해주실수 있나요?
이건 학교 다닐 때 과제로 짠거라 짜는 김에 포스팅한 것입니다만..
지금은 딱히 알고리즘 관련 포스팅 할만한 이유가 없다보니...
자료구조 공부를 하다가 우선순위의 개념을 이용한 허프만 코드에
흥미가 생겨서 인터넷으로 찾아보던 중.. 좋은 글을 찾게되어 영광입니다.
혹시 페이스북 하시면 친구 부탁드려도 될까요???
바꾸면 돌아가네요
c++에서 문제가 생길 수도 있습니다.
안되길래 복사를 해도 그러네요.
중간에 뭐가 문제가된거지 ㄷㄷ
저 같은 경우는 문자열을 쭉 주고 거기서 빈도수를 계산 -> 트리구현 -> 코드생성 단계로 갈려고 하는데 트리에서 막혀있었거든요.
우선순위 큐 쓰셔서 구현하신거 참고 많이 됬습니다!!
main 함수 내에 pq에 삽입하기 위해 node* r을 생성했는데 이거는 whilde문 내에서 삽입용으로만 사용하는거죠? 그렇다면 while문이 끝나면 해제해주는게 좋지 않나요? r을 해제해주는 free가 안보이네요. 윗분들 답변중에 잇는지 모르겠네요 ㅎ;
while 안에서 malloc한 영역은 while 끝난 뒤에도 계속 사용됩니다.
[PQ [node] ] 이런 식인데 안의 [node]는 while 안에서 r 생성한 것을 그대로 가지고 있어야 하니
PQ와 트리를 다 쓸 때 까지 free하면 안되고 마지막에 freeTree하면서 일괄로 해제되므로 문제 없고
제 생각에는 오히려 insert에서 동적할당한 저 [PQ ]들을 free하는 부분이 필요한 거 같습니다.
파일을 따로 저장하는 방식이 아닌 scanf 를 통해 문자열을 입력받고 해석하려면
어느 부분을 수정해야 할까요? file 관련 부분이 이곳저곳 흩어져있어서 함부로 건드리기가 겁나네요
while 문 시작 전에 fopen 대신에 scanf로 문자열을 받으시면 될 것 같고요.
while 문 안에서 fscanf(file, "%c:%d ", &r->symbol, &r->frequency) 대신 할 코드를 짜시면 될 것 같습니다.
공부하는 방법을 알려주셔서 감사합니다.
제가 배우는 책에는 최소히프를 이용해서 만들었는데, 우선순위큐로도 할 수 있군요?
훨씬 깔끔해보이는게 해봐야만 할것 같네요.
그리구요 궁금한게 있습니다.
print_tree에 쓰이는 마지막 매개변수 code부분이요.
main함수에서 malloc으로 char 포인터만 생성해주셨잖아요.
이걸 이대로 print_tree함수에 넣으면, 함수에서 자동으로 새로운 공간을 할당하나요?
제가 보기에는 code[0]을 제외한 나머지 부분은 할당되지 않은 메모리를 참조하고 있는것으로 생각되는데,
제가 잘못알고 있는걸까요?
변수 하나로 모든 child의 값을 출력하는 방법은 오늘 처음 보는데 되게 신기하네요 ㅎㅎ
제가 이 코드를 짠 게 6년도 더 지난 옛날(?)이고, 졸업 이후에 C, C++ 개발을 하고 있지 않아서 제 앎의 깊이가 더 깊어지지는 못했거든요.
근데 gcc가 좀 관대 한건지. visual c에서는 컴파일 에러가 나거나 문제가 생기는 부분도 정상적으로 컴파일이 되기도 하더라구요.
그리고 제가 이 소스를 공개하게 된 계기는 그 당시 과제를 할 때 돈을 주고 사지 않으면 참고할 소스가 딱히 없었기 때문에, 그냥 내가 열심히 짜서 공개하겠다는 생각이 강했던 것이지 제가 짠 코드가 완벽하다라고 생각해서 공개한 건 아니라는 점을 알아주셨으면 합니다. ^^
C의 관대함이 참아주고 있는건 알고 있었는데,
혹시 제가 모르는 무언가가 있었나 싶어서요 ㅎ
저 상태에서 code변수 이후에 새로운 변수가 추가되면 아마 해당 변수에 문제가 생길거에요.
소스 보시는 분들 참고 하시라고 남겨둡니다.
도움 잘 받았습니다.
감사합니다.(--)(__)(--)
1byte짜리 alloc을 한다음에, print_tree에 넘겨준다...
..
혹시 이유를 알고 계신가여? 당최 모르겠네요....
..
char* code = (char*) malloc(sizeof(char));
그럼 말이 되는 듯한데요.
알고리즘 잘보고 갑니다.
insert시 ordering을 하는 방법고,
remove로 min node를 가지고 오는 방식은 기가막힌 아이디어네요.
혹시 해보신적 있나요? .pdf 나 .pptx 같은 경우요