
출처 movierg gianaa

인기 만화들 다있어요 ~~ ㅎㅎ
부록은 다운로드 프로그램..
버그가 아직은 좀. 몇몇 이미지는 받기가 힘들어요. 특정 만화는 반자동 ㅋ
< 목 차 >
1 Prologue 3
2 Introduction 4
3 Run-Length 6
3.1 Run-Length 압축 알고리즘 6
3.2 Run-Length 압축 복원 알고리즘 10
3.3 Run-Length 압축 알고리즘 전체 구현 11
4 Lempel-Ziv 19
4.1 Lempel-Ziv 압축 알고리즘 19
4.2 Lempel-Ziv 압축 복원 알고리즘 26
4.3 Sliding Window를 이용한 Lempel-Ziv 알고리즘의 구현 27
5 Variable Length 39
6 Huffman Tree 43
6.1 Huffman 압축 알고리즘 51
6.2 Huffman 압축 복원 알고리즘 56
6.3 Huffman 압축 알고리즘 구현 60
7 JPEG (Joint Photographic Experts Group) 72
7.1 JPEG이란 72
7.2 다른 기술과의 비교 72
7.3 압축 방법 73
7.4 Baseline 압축 알고리즘 75
7.5 JPEG의 실제 압축 / 복원 과정 76
7.6 확장 JPEG 79
8 MPEG (Moving Picture Expert Group) 80
8.1 MPEG의 개념 80
8.2 MPEG의 표준 81
8.2.1 MPEG 1 81
8.2.2 MPEG 2 82
8.2.3 MPEG 4 83
8.3 MPEG의 기본적인 압축 원리 84
8.3.1 시간,공간의 중복성 제거 84
8.3.2 I,P,B영상 86
9 Conclusion 87
< 그 림 목 차 >
<그림 3‑1> Run-Length 압축 알고리즘 10
<그림 3‑2> 압축 파일 헤더 구조 12
<그림 4‑1> 슬라이딩 윈도우와 해시테이블 22
<그림 5‑1> 8비트에서 7비트로 줄이는 압축 알고리즘 39
<그림 5‑2> 문자 코드의 재구성 40
<그림 5‑3> <그림 5‑2>코드의 기수 나무 41
<그림 5‑4> 문자 코드의 재구성 41
<그림 6‑1> 빈도수 계산 44
<그림 6‑2> 허프만 나무 구성과정 48
<그림 6‑3> 허프만 나무에서 얻어진 코드 51
<그림 6‑4> code[]와 len[]의 저장 55
<그림 7‑1> JPEG Encoding / Decoding 단계 76
<그림 7‑2> RGB의 YIQ 변환식 77
1 Prologue
지금 생각하면 우스운 일이지만 몇 년 전만 하더라도 28800bps의 모뎀을 굉장히 빠른 통신 장비로 알고 있었다. 그러다가
56600bps의 모뎀이 발표되었을 때는 전화선의 한계를 뛰어 넘은 대단한 물건이라고 다들 놀라와 했다. 내 경우에도
56600bps 모뎀을 구입해서 처음 사용하던 날 감격의 눈물을 흘렸을 정도였으니..
전화로 통신을 하던 그 당시 사람들의 생각은 다들 비슷했을 것이다. 어떻게 하면 같은 내용의 자료를 더 짧은 시간에 전송할 수
있을까. 통신속도가 점차 빨라지면서(처음에 사용하던 2400bps에 비하면 거의 20배 이상의 속도 향상이었다.) 이런 고민은
줄어들 것이라 생각했지만, 그런 고민은 오히려 더 커져 만 갔다. 속도가 빨라지는 것보다 사람들이 주고받는 자료의 전송 량이 더
크게 증가한 것이다. 이럴 수록 더 강조되던 것이 바로 [압축] 이었다.
파일 압축이라고 하면 winzip, alzip 등을 생각할 것이다. 이런 종류의 프로그램들은 임의의 파일을 원래의 크기보다 작은 크기로 압축시켰다가 필요할 때 다시 원래대로 한치의 오차도 없이 복구 시켜 준다.
하지만 압축이란 것이 모두 앞에서 언급한 프로그램들처럼 원본을 그대로 복원해줄 수 있는 것이 아니다. 때에 따라서는 원본으로의 복원이 불가능한 압축 방법들이 유용하게 사용될 상황도 존재한다.
전자의 경우를 ‘비손실 압축’, 후자의 경우를 ‘손실 압축’ 이라고 하는데, 이 자료에서는 모든 압축의 근간이 되는 간단한 압축 알고리즘들을 살펴볼 것이고 뒤에 손실 압축의 대표적인 MPEG에 대해서 다룰 것이다.
이제 우리는 압축의 세계로 들어간다.
2 Introduction
우리가 보통 살펴보는 알고리즘들은 대부분이 시간을 절약하기 위한 목적을 가지고 개발된 것 들이다. 하지만, 우리가 지금부터 살펴볼 알고리즘들은 공간을 절약하기 위한 목적을 가진 알고리즘이다.
압축알고리즘이 처음으로 대두되기 시작한 것은 컴퓨터 통신 때문이었다. 컴퓨터 통신에서는 시간이 곧바로 돈으로 연결된다(적어도
model을 사용하던 시절에는 그랬다). 예를 들어 1MByte의 파일을 다운로드 받으려면 28,800bps 모뎀을 사용하면 약
6분, 56,600bps 모뎀을 사용하더라도 약 3분 이상의 시간이 소요됐었다. 하지만 이 파일을 전송 전에 미리 1/2로만
압축할 수 있다면 전송시간 역시 1/2로 줄어들 것이다. 즉, 통신 비용 역시 1/2로 줄어든다는 것이다.
압축 알고리즘은 크게 두 부류로 나뉜다. 비손실 압축(Non-lossy Compression)과 손실 압축(Lossy
Compression)이 그것인데 말 그대로 비손실 압축은 압축했다가 다시 복원할 때 원래대로 파일이 복구된다는 뜻이고, 손실
압축은 복원할 때 100% 원래대로 복구되지 않는다는 뜻이다.
일반적으로 PC사용자들이 사용하는 압축프로그램들은 모두 비손실 압축을 지원한 프로그램들이다. 그렇다면 손실 압축은 어떤 경우에 사용하는 것일까?
확장자가 exe나 com으로 끝나는 실행파일이나, 기타 한 바이트만 바뀌더라도 프로그램 실행에 지장을 주는 파일들은 반드시
비손실 압축을 해야 한다. 그러나 그림 파일이나 동화상처럼 눈으로 보는 것에 지나지 않는 파일의 경우 약간의 손실이 있어도
무방하다.
일반적으로 손실 압축이 비손실 압축에 비해서 압축률이 훨씬 좋기 때문에 손실 압축도 또한 큰 중요성을 가지고 있다. 요즘 화제가
되고 있는 JPEG(정지 화상 압축 기술, Joint Photographic Expert Group), MPEG(동화상 압축
기술, Moving Picture Expert Group) 등도 대표적인 손실 압축법으로 주목 받고 있는 것들이다.
압축 알고리즘은 그 중요성으로 인해 오랫동안 연구되어 왔고, 많은 알고리즘이 있다. 가장 대표적인 압축 알고리즘은
Run-Length 압축법으로 동일한 바이트가 연속해 있을 경우 이를 그 바이트와 몇 번 반복되는지 수치를 기록하는 방법이다.
그러나 Run-Length 압축법은 간단함에 대한 대가로 압축률이 그다지 좋지 않아서 다른 방법들이 연구되어 왔다.
그래서 실제로 구현되는 압축 방법은 이 절에서 소개하는 Huffman 압축법과 Lempel-Ziv 압축법이다. 가변길이 압축법은
한 바이트가 8비트라는 고정 관념을 깨고, 각각을 다른 비트로 압축하는 방법이고, 그 중에서도 Huffman 압축법은 빈도가
높은 바이트는 적은 비트수로, 빈도가 낮은 바이트는 많은 비트수로 그 표현을 재정의하여 파일을 압축한다.
반면에 Lempel-Ziv법은 그 변종이 여러 개 있지만 가장 효율적인 동적 사전(Dynamic Dictionary)을 이용한
방법을 주로 사용한다. 동적 사전법은 파일에서 출현하는 단어(Word)들을 2진 나무(Binary Tree)나 해시를 이용한
검색 구조에 삽입하여 동적 사전을 구성한 다음, 이어서 읽어진 단어가 동적 사전에 수록되어 있으면 그에 대한 포인터를 그
내용으로 대체하는 방법으로 압축을 행한다. 주로 사용하는 ZIP 등도 Huffman 압축법이나 Lempel-Ziv 압축법 중
하나를 사용하거나 또는 둘 다 사용하거나, 혹은 그 응용을 사용한다.
3 Run-Length
3.1 Run-Length Encoding
Run-Length 압축법은 동일한 문자가 이어서 반복되는 경우 그것을 문자와 개수의 쌍으로 치환하는 방법이다. 예를 들어 다음의 문자열은 Run-Length 압축법으로 쉽게 압축될 수 있다.
원래 문자열 : ABAAAAABCBDDDDDDDABC
압축 문자열 : ABA5BCBD7ABC
개
념적으로는 위와 같이 간단하지만 개수로 사용된 5나 7이라는 문자가 개수의 의미인지 아니면 그냥 문자인지를 판별하는 방법이
없다. 만일 압축할 파일이 알파벳 문자만을 사용한다면 위와 같은 압축이 그대로 사용 가능할 것이다. 그러나 일반적으로 0부터
255까지의 모든 문자가 사용된 파일을 압축한다면 단순한 위의 방법으로는 압축이 불가능하다.
그래서 탈출 문자(Escape Code)라는 것을 사용한다. 문자가 반복되는 모양을 압축할 때 <탈출 문자, 반복 문자,
개수>와 같이 표현한다. 예를 들어 탈출 문자를 ‘*’라고 한다면 위의 문자열은 다음처럼 압축 될 수 있다.
원래 문자열 : ABAAAAABCBDDDDDDDABC
압축 문자열 : AB*A5BCB*D7ABC
탈
출 문자에서 탈출의 의미는 보통의 경우에서 벗어남을 말한다. 즉 탈출 문자 ‘*’가 나오기 전에는 단순한 문자열이지만 이 탈출
문자가 나오면 그 다음의 반복 문자와 그 다음의 개수를 읽어 들여서 반복 문자를 개수만큼 늘여 해석하면 된다.
또 한가지 남은 문제가 있다. 그것은 탈출 문자가 탈출의 의미로 해석되는 것이 아니라 문자로서 해석되어야 할 경우도 있다는
점이다. 이것은 마치 printf() 함수의 서식 문자열에서 ‘%’와 유사하다. %d나 %f는 그 문자를 의미하는 것이 아니라
정수나 실수형으로 대치될 부분이라는 표시이다. 즉 %가 탈출의 의미를 가지고 있다는 뜻이다. 그러나 정작 ‘%’라는 문자를
출력하기 위해서는 어떻게 해야 하는가?
C에서는 ‘%’를 출력하기 위해서 ‘%%’를 사용한다. 마찬가지로 Run-Length 압축법에서도 탈출 문자 ‘*’를 문자로 해석하기 위해서 ‘**’를 사용하면 될 것이다.
그렇다면 ‘*’ 문자가 계속해서 반복되는 경우는 어떻게 해야 하는가? 이 문제는 상당히 복잡하다. 만일 ‘*****’와 같은
문자열의 일부분이 있다면 ‘**5’와 같이 압축할 수 있는가? 아니면 ‘***5’와 같이 압축하는가? 둘 다 문제가 있다.
전자의 경우 ‘*5’와 같이 해석할 수 있으며, 후자의 경우는 ‘*’문자와 5 다음의 문자가 있다면 이를 개수로 해석해서 5를
반복하는 것으로 해석할 수 있다.
이렇게 탈출 문자가 반복되는 경우 그것을 <탈출 문자 반복 문자 개수>의 표현으로 나타내면 모호하게 되므로 탈출
문자자의 경우는 아무리 반복 횟수가 많더라도 단순하게 <탈출 문자, 탈출 문자>와 같이 압축한다(실제로는 더
길어지지만).
원래 문자열 : ABCAAAAABCDEBBBBBFG*****ABC
압축 문자열 : ABC*A5BCDE*B5FB**********ABC
이
러한 이유로 탈출 문자 ‘*’는 가장 출현 빈도수가 적은 문자를 택해야 한다. 왜냐하면 탈출 문자가 문자로 해석되는 경우에는 그
길이가 두 배로 늘어나기 때문이다. 이 출현 빈도수라는 것이 사실 모호하기 짝이 없지만 일단은 영어의 알파벳이나 기호, 탭
문자(0x09), 라인 피드(0x0A), 캐리지 리턴(0x0D) 그리고 널문자(0x00)와 같은 코드들은 매우 많이 사용되기
때문에 피해야 한다. 따라서, 압축하는 파일에 따라 탈출 문자를 적절히 조정해 주면 압축 효율을 높일 수 있을 것이다.
그렇다면 과연 몇 개의 문자가 반복되었을 때 <탈출 문자, 반복 문자, 개수>로 치환할 것인가 하는 문제를 결정하자.
‘AA’처럼 두 문자가 반복되었다면 ‘*A2’로 하는 것은 두 바이트가 3바이트로 늘어나게 되므로 치환하지 말아야 할 것이다.
그렇다면 ‘AAA’와 같이 세 문자가 반복된다면 ‘*A3’으로 하는 것은 똑같이 세 바이트가 소요되므로 치환을 하든 하지 않든
변화가 없다. 따라서 같은 문자가 최소 3번 이상 반복되는 경우에만 치환을 하도록 한다.
그리고 개수를 나타내는 것 또한 1Byte를 사용하기 때문에 반복되는 문자의 개수는 255 이상이 될 수 없다. 만약 255개를 넘어버린다면 254에서 한번 잘라주고, 그 다음은 문자가 처음 나온 것으로 생각하면 된다.
위와 같은 방법으로 구현된 Run-Length 알고리즘은 다음과 같다.
<Run-Length 압축 알고리즘(FILE *src)
{
char code[10]; /* 버퍼 */
cur = getc(src); /* 입력 파일에서 한 바이트 읽음 */
code_len = length = 0;
while(!feof(src))
{
if (length == 0) /* code[]에 아무 내용이 없으면 */
{
if (cur != ESCAPE)
{
code[code_len++] = cur;
length++;
}
else /* 탈출 문자이면 <탈출문자 탈출문자>로 대체 */
{
code[code_len++] = ESCAPE;
code[code_len++] = ESCAPE;
flush(code); /* 출력 파일에 써넣음 */
length = code_len = 0;
}
cur = getc(src);
}
else if (length == 1) /* 반복 횟수가 1 이었으면 */
{
if (cur != code[0]) /* 읽은 문자가 버퍼의 문자와 다르면 */
{
flush(code); /* 버퍼의 내용을 출력 */
length = code_len = 0;
}
else /* 읽은 문자가 버퍼의 문자와 같으면 */
{
length++;
code[code_len++] = cur; /* 'A' -> 'AA' */
cur = getc(src);
}
}
else if (length == 2) /* 반복 횟수가 2 이면 */
{
if (cur != code[1]) /* 읽은 문자가 버퍼의 문자와 다를 경우 */
{
flush(code); /* 버퍼의 내용을 출력 */
length = code_len = 0;
}
else /* 읽은 문자가 버퍼의 문자와 같으면 */
{
length++;
code_len = 0;
code[code_len++] = ESCAPE; /* 'AA' -> '*A3' */
code[code_len++] = cur;
code[code_len++] = length;
cur = getc(src); /* 다음 문자를 읽음 */
}
}
else if (length > 2) /* 반복 횟수가 3 이상이면 */
{
if (cur != code[1] || length > 254)
{ /* 읽은 문자 != 버퍼의 문자 or 반복 횟수 > 255 */
flush(code); /* 버퍼의 내용 출력 */
length = code_len = 0;
}
else /* 읽은 문자가 버퍼의 문자와 같으면 */
{
code[code_len-1]++; /* 반복 횟수만 증가 */
length++;
cur = getc(src); /* 다음 문자를 읽음 */
}
}
}
flush(code); /* 버퍼의 내용을 출력 */
}
<그림 3‑1> Run-Length 압축 알고리즘
3.2 Run-Length Decoding
압축을 하고 나면 다시 복원을 하는 알고리즘도 있어야 할 것이다. Run-Length 압축법의 복원은 상당히 단순하다. 파일을
읽으면서 탈출 문자가 없으면 그대로 두면 되고, 탈출 문자를 만난다면, 다음 글자를 하나 더 읽어봐서 다시 탈출 문자가 나오면
탈출 문자를 그대로 기록하고, 숫자가 나오면 탈출 문자 전의 문자를 그 숫자만큼 반복해서 적으면 된다.
위와 같은 방법으로 구현된 Run-Length 압축 복원 알고리즘은 다음과 같다.
<Run-Length 압축 풀기 알고리즘(FILE *src)>
{
int cur;
FILE *dst;
int j;
int length;
dst = fopen(출력파일);
cur = getc(src);
while (!feof(src))
{
if (cur != ESCAPE) /* 탈출 문자가 아니면 */
putc(cur, dst);
else /* 탈출 문자이면 */
{
cur = getc(src);
if (cur == ESCAPE) /* 그 다음 문자도 탈출 문자이면 */
putc(ESCAPE, dst);
else /* 길이만큼 반복 */
{
length = getc(src);
for (j = 0; j < length; j++)
putc(cur, dst);
}
}
cur = getc(src);
}
fclose(dst);
}
3.3 Run-Length 압축 알고리즘 전체 구현
실제로 압축된 파일의 복원을 위해서는 몇 가지 추가적인 정보가 필요하다. 그것은 복원하려는 파일이 과연 Run-Length 압축
알고리즘에 의한 것인지를 판별하는 식별 코드와 복원할 파일의 원래 이름이다. 이 두 정보는 압축할 때 압축 파일의 선두(헤더)에
기록되어 있어야 한다.
Run-Length 압축 알고리즘의 식별 코드는 편의상 0x11과 0x22로 했고, 이어서 원래 파일의 이름이 나오고, 끝을 나타내는 NULL문자가 이어진다. 다음은 이 헤더의 구조를 나타낸 그림이다.
<그림 3‑2> 압축 파일 헤더 구조
이
상으로 Run-Length 압축 알고리즘에 대한 설명을 마친다. Run-Length 알고리즘은 알고리즘이 단순할 뿐만 아니라
이미지 파일이나 exe 파일처럼 똑같은 문자가 반복되는 경우 매우 좋은 압축률을 보여준다. 그러나 똑같은 문자가 이어져 있지
않은 경우에는 압축률이 매우 떨어지는 단점이 있다.
위와 같은 방법으로 구현된 전체 Run-Length 알고리즘은 다음과 같다.
/* */
/* RUNLEN.C : Compression by Run-Length Encoding */
/* */
#include <stdio.h>
#include <string.h>
#include <dir.h>
#include <time.h>
#include <stdlib.h>
/* 탈출 문자 */
#define ESCAPE 0xB4
/* Run-Length 압축법에 의한 것임을 나타내는 식별 코드 */
#define IDENT1 0x11
#define IDENT2 0x22
/* srcname[]에서 파일 이름만 뽑아내어서 그것의 확장자를 rle로 바꿈 */
void make_dstname(char dstname[], char srcname[])
{
char temp[256];
fnsplit(srcname, temp, temp, dstname, temp);
strcat(dstname, ".rle");
}
/* 파일의 이름을 받아 그 파일의 길이를 되돌림 */
long file_length(char filename[])
{
FILE *fp;
long l;
if ((fp = fopen(filename, "rb")) == NULL)
return 0L;
fseek(fp, 0, SEEK_END);
l = ftell(fp);
fclose(fp);
return l;
}
/* code[] 배열의 내용을 출력함 */
void flush(char code[], int len, FILE *fp)
{
int i;
for (i = 0; i < len; i++)
putc(code[i], fp);
}
/* Run-Length 압축 함수 */
void run_length_comp(FILE *src, char *srcname)
{
int cur;
int code_len;
int length;
unsigned char code[10];
char dstname[13];
FILE *dst;
make_dstname(dstname, srcname);
if ((dst = fopen(dstname, "wb")) == NULL) /* 출력 파일 오픈 */
{
printf("\n Error : Can't create file.");
fcloseall();
exit(1);
}
/* 압축 파일의 헤더 작성 */
putc(IDENT1, dst); /* 출력 파일에 식별자 삽입 */
putc(IDENT2, dst);
fputs(srcname, dst); /* 출력 파일에 파일 이름 삽입 */
putc(NULL, dst); /* NULL 문자 삽입 */
cur = getc(src);
code_len = length = 0;
while (!feof(src))
{
if (length == 0)
{
if (cur != ESCAPE)
{
code[code_len++] = cur;
length++;
}
else
{
code[code_len++] = ESCAPE;
code[code_len++] = ESCAPE;
flush(code, code_len, dst);
length = code_len = 0;
}
cur = getc(src);
}
else if (length == 1)
{
if (cur != code[0])
{
flush(code, code_len, dst);
length = code_len = 0;
}
else
{
length++;
code[code_len++] = cur;
cur = getc(src);
}
}
else if (length == 2)
{
if (cur != code[1])
{
flush(code, code_len, dst);
length = code_len = 0;
}
else
{
length++;
code_len = 0;
code[code_len++] = ESCAPE;
code[code_len++] = cur;
code[code_len++] = length;
cur = getc(src);
}
}
else if (length > 2)
{
if (cur != code[1] || length > 254)
{
flush(code, code_len, dst);
length = code_len = 0;
}
else
{
code[code_len-1]++;
length++;
cur = getc(src);
}
}
}
flush(code, code_len, dst);
fclose(dst);
}
/* Run-Length 압축을 복원 */
void run_length_decomp(FILE *src)
{
int cur;
char srcname[13];
FILE *dst;
int i = 0, j;
int length;
cur = getc(src);
if (cur != IDENT1 || getc(src) != IDENT2) /* Run-Length 압축 파일이 맞는지 확인 */
{
printf("\n Error : That file is not Run-Length Encoding file");
fcloseall();
exit(1);
}
while ((cur = getc(src)) != NULL) /* 헤더에서 파일 이름을 얻음 */
srcname[i++] = cur;
srcname[i] = NULL;
if ((dst = fopen(srcname, "wb")) == NULL)
{
printf("\n Error : Disk full? ");
fcloseall();
exit(1);
}
cur = getc(src);
while (!feof(src))
{
if (cur != ESCAPE)
putc(cur, dst);
else
{
cur = getc(src);
if (cur == ESCAPE)
putc(ESCAPE, dst);
else
{
length = getc(src);
for (j = 0; j < length; j++)
putc(cur, dst);
}
}
cur = getc(src);
}
fclose(dst);
}
void main(int argc, char *argv[])
{
FILE *src;
long s, d;
char dstname[13];
clock_t tstart, tend;
/* 사용법 출력 */
if (argc < 3)
{
printf("\n Usage : RUNLEN <a or x> <filename>");
exit(1);
}
tstart = clock(); /* 시작 시각 기록 */
s = file_length(argv[2]); /* 원래 파일의 크기를 구함 */
if ((src = fopen(argv[2], "rb")) == NULL)
{
printf("\n Error : That file does not exist.");
exit(1);
}
if (strcmp(argv[1], "a") == 0) /* 압축 */
{
run_length_comp(src, argv[2]);
make_dstname(dstname, argv[2]);
d = file_length(dstname); /* 압축 파일의 크기를 구함 */
printf("\nFile compressed to %d%%", (int)((double)d/s*100.));
}
else if (strcmp(argv[1], "x") == 0) /* 압축의 해제 */
{
run_length_decomp(src);
printf("\nFile decompressed & created.");
}
fclose(src);
tend = clock(); /* 종료 시각 기록 */
printf("\nTime elapsed %d ticks", tend - tstart); /* 수행 시간 출력 : 단위 tick */
}
3.4 실행 결과
filetype Run-Length
random-bin 100.59
random-txt 100.24
wave 98.20
pdf 99.03
text(big) 85.04
text(small) 98.71
sql 96.78
Run-Length 알고리즘의 특성 때문에 Random 파일에 대해서는 오히려 파일 크기가 증가하는 결과가 나타났다. 다른 경우에는 조금씩 압축이 되었으며, 크기가 큰 텍스트 파일에 대해서는 상당히 많은 압축이 되었다. 이것은 텍스트 파일에 들어있는 연속된 Space나 Enter 등을 압축 한 것으로 해석된다. SQL 역시 Space가 많아서 압축이 되었을 것이라 생각한다.
4 Lempel-Ziv
4.1 Lempel-Ziv Encoding
Run-Length 압축 알고리즘도 실제로 많이 사용되지만, 이 절에서 소개하는 Lempel-Ziv 알고리즘 또한 실제에서 가장 많이 사용되는 매우 우수한 압축 알고리즘이다.
Run-Length 알고리즘은 똑같은 문자가 반복되는 경우 그것을 <탈출 문자, 반복 문자, 반복 횟수>로 치환하는
방법이었다. 이와 유사하게 Lempel-Ziv 압축법은 현재의 패턴이 가까운 거리에 존재한다면 그것에 대한 상재적 위치와 그
패턴의 길이를 구해서 <탈출 문자, 상대 위치, 길이>로 패턴을 대치하는 방법이다.
원래 문자열 : ABCDEFGHIJKBCDEFJKLDM
압축 문자열 : ABCDEFGHIJK<10,5>JKLDM
위
의 그림을 보면, 원래 문자열에서 ‘BCDEF’라는 패턴이 뒤에 다시 반복된다. 이 때 뒤의 패턴을 <10,5>와
같이 10문자 앞에서 5문자를 취하라는 코드를 삽입함으로써 압축할 수 있고, 그 반대로 복원 할 수도 있다.
이렇게 떨어진 두 패턴뿐만 아니라 서로 겹쳐있는 패턴에 대해서도 이런 표현이 가능하다.
원래 문자열 : CDEFABABABABABAJKL
압축 문자열 : CDEFAB<2,9>JKL
원래 문자열 : CDEFAAAAAAAJKL
압축 문자열 : CDEFA<1,7>JKL
두
번째 예를 보면 Lempel-Ziv 압축법은 Run-Length 압축법과 마찬가지로 동일한 문자의 반복에 대해서도
Run-Length 압축법과 비슷한 압축률을 보임을 알 수 있다. 게다가 첫 번째와 같이 동일한 패턴이 반복되는 경우
Run-Length로는 압축하기 곤란하지만 Lempel-Ziv 압축법에서는 간단하게 압축된다.
이렇게 간단한 원리는 Lempel-Ziv 압축법은 그 실제 구현에서 여러 가지 다양한 방법이 있다. 가장 대표적인 방법은 정적 사전(Static Dictionary)법과 동적 사전(Dynamic Dictionary)법이다.
정적 사전법은 출현될 것으로 예상되는 패턴에 대한 정적 테이블을 미리 만들어 두었다가 그 패턴이 나올 경우 정적 테이블에 대한 참조를 하도록 하여 압축하는 방법이다.
이 방법은 압축하고자 하는 파일의 내용이 예상 가능한 경우에 매우 좋은 방법이다. 예를 들어 C의 소스 파일만을 압축하고자 할
경우 C의 예약어와 출현 빈도가 높은 식별자(Identifier)에 대해 테이블을 미리 만들어 둔다면 매우 높은 효율과 빠른
속도의 압축을 할 수 있을 것이다. 그러나 임의의 파일을 압축하고자 할 때에는 그 효율을 장담하지 못한다.
동적 사전법은 파일을 읽어들이는 과정에서 패턴에 대한 사전을 만든다. 즉 동적 사전법에서 패턴에 대한 참조는 이미 그전에 파일
내에서 출현한 패턴에 한한다. 동적 사전법은 파일을 읽어들이면서 사전을 구성해야 하는 부담이 생기기 때문에 속도가 느리다는
단점이 있으나, 임의의 파일에 대해 압축률이 좋은 경우가 많다.
우리는 정적 사전법은 동적 사전법과 별로 다를 것이 없으므로 동적 사전법만 다루기로 한다.
동적 사전법을 실제로 구현하는데 있어 가장 중요한 자료 구조는 Sliding Window이다. Sliding Window는 전체
파일의 일부분을 FIFO(First In First Out) 구조의 메모리에 유지하고 있는 것을 의미한다. 그리고 이
Sliding Window는 파일에서 문자를 읽을 때마다 파일 내에서의 상대 위치가 끝 쪽으로 전진하게 된다.
그리고 Sliding Window는 윈도우 내의 어떤 부분에 원하는 패턴이 있는지 찾아낼 수 있는 검색 구조까지 갖추고 있어야 한다.
Sliding
Window의 FIFO 구조 때문에 가장 적절하게 사용될 수 있는 구조는 원형 큐(Circular Queue)이다. 그리고
Sliding Window의 검색 구조는 주로 해쉬(Hash)나 2진 나무(Binary Tree)를 사용한다.
일반적으로 FIFO 구조(Sliding Window)의 크기는 압축률에 상당한 영향을 미치며, 검색 구조는 압축 속도에 큰
영향을 미친다. 즉 Sliding Window가 크면 동적 사전이 그만큼 더 방대하게 구성되어서 패턴을 찾아낼 확률이 크게
되고, 검색 구조가 효율적일수록 패턴을 빨리 찾아내기 때문이다.
이 자료에서 작성할 Lempel-Ziv 압축법은 원형 큐와 한 문자에 대한 해시(연결법)로 패턴을 찾아낸다.
설명을 위해 다음 그림을 보자
<그림 4‑1> Sliding Window와 해시테이블
<그림 4‑1> (가) 그림은 큐
queue[]의 모양을 보여준다. 큐에는 압축할 파일에서 문자를 하나씩 읽어서 저장해 놓는다. front는 큐의 get() 명령
시 빠져나올 원소의 위치이고, rear는 큐의 put() 명령 시 새 원소가 들어갈 위치를 의미한다. 그리고 cp는 찾고자 하는
패턴이고, sp는 cp위치에 있는 패턴과 일치하는 앞쪽의 패턴 위치를 저장하고 있다. 그리고 length는 일치한 패턴의 길이를
의미하고 (가) 그림에서는 5가 된다.
(나) 그림은 해시 테이블 jump_table[]의 모습이다. jump_table[]은 큐에 있는 문자가 어느 위치에 있는지
바로 찾을 수 있도록 큐에서의 위치들을 연결 리스트로 구성하고 있다. 예를 들어 ‘G’라는 문자를 큐 내에서 찾으려면 선형
검색처럼 처음부터 끝까지 검색해야 하는 것이 아니라, jump_table[‘G’]로서 연결 리스트의 시작 위치를 찾은 다음 연결
리스트를 타고 가면 14의 위치와 9의 위치에 ‘G’라는 문자가 있음을 알 수 있다.
참고로 Lempel-Ziv 압축법에서는 패턴을 <탈출문자 상대위치 패턴길이>로 나타내는데 이 자료에서는 상대 위치와
패턴 길이 모두 1바이트를 사용한다. 즉 상대 위치는 앞으로 255만큼, 패턴의 길이도 255만큼이 가능하다는 이야기다. 패턴을
찾는 장소가 바로 큐이기 때문에 큐의 길이도 255보다 큰 것은 아무 의미가 없다. 이렇게 상대 위치와 패턴의 길이를 몇 비트로
나타낼 것인가에 따라 큐의 크기를 정해 준다.
Sliding Window에서 가장 핵심적인 부분은 원하는 패턴을 찾아내는 함수이다. 이 부분은 다음의 qmatch() 함수에
구현되어 있다. 이 qmatch() 함수는 Lempel-Ziv 압축법에서 압축 시에 가장 많이 호출되고 가장 많이 시간이
소요되는 부분이므로 충분히 최적화되어 있어야 한다.
int qmatch(int length)
{
int i;
jump *t, *p;
int cp, sp;
cp = qx(rear - length); // cp의 설정
p = jump_table + queue[cp];
t = p->next;
while (t != NULL)
{
sp = t->index; // sp의 설정, 해시 테이블에서 바로 읽어온다
for (i = 1; i < length && queue[qx(sp+i)] == queue[qx(cp+i)]; i++);
if (i == length) return sp;
// 패턴을 찾았으면 sp를 되돌림
t = t->next; // 패턴 검색에 실패했으면 다음 위치로 이동
}
return FAIL; // 패턴이 큐 내에 없음
}
qmatch() 함수는 결국 cp와 length로 주어지는 패턴을 큐 내에서 찾아서 그 위치 sp를 되돌려주는 기능을 한다.
<Sliding Window를 이용한 LZ 압축 알고리즘(FILE *src, char *srcname)>
{
FILE *dst = 출력파일;
jump_table[] 초기화;
init_queue();
put(getc(src));
length = 0;
while (!feof(src))
{
if (queue_full())
{
if (sp == front) /* 현재 추정된 패턴이 큐에서 벗어나려 하면 */
{ /* 현재까지의 정보로 출력 파일에 쓴다 */
if (length > 3) /* 패턴의 길이가 4 이상이면 압축 */
encode(sp, cp, length, dst);
else /* 아니면 그냥 씀 */
for (i = 0; i < length; i++)
{
put_jump(queue[qx(cp+i)], qx(cp+i));
/* 다음을 위해 jump_table[]에 문자들의 */
/* 위치를 기록 */
putc1(queue[qx(cp+i)], dst);
}
length = 0;
}
del_jump(queue[front], front);
/* 큐에서 빠져 나온 문자는 jump_table[]에서 제거 */
get(); /* 큐에서 문자 하나를 뺀다 */
}
if (length == 0)
{
cp = qx(rear-1); /* cp의 설정, 가장 최근에 들어온 문자 */
sp = qmatch(length+1); /* 패턴을 찾아 sp에 줌, 길이는 1 */
if (sp == FAIL) /* 패턴 검색에 실패했으면 */
{
putc1(queue[cp], dst); /* 출력 파일에 기록 */
put_jump(queue[cp], cp);
}
else
length++;
put(getc(src)); /* 다음 문자를 입력 파일에서 읽어 큐에 집어넣음 */
}
else if (length > 0) /* 패턴의 길이가 1 이상이면 */
{
if (queue[qx(cp+length)] != queue[qx(sp+length)])
j = qmatch(length+1); /* 새로 들어온 문자까지 포함해서 */
/* 패턴의 위치를 다시 검색 */
else j = sp;
if (j == FAIL || length > SIZE - 3)
{ /* 실패했으면 현재까지의 정보로 압축을 함 */
if (length > 3)
{
encode(sp, cp, length, dst);
length = 0;
}
else
{
for (i = 0; i < length; i++)
{
put_jump(queue[qx(cp+i)], qx(cp+i));
putc1(queue[qx(cp+i)], dst);
}
length = 0;
}
}
else /* 패턴 검색에 성공했으면 */
{
sp = j;
length++; /* 길이를 1증가 */
put(getc(src)); /* 큐에 새 문자를 집어넣음 */
}
}
}
/* 큐에 남아있는 문자들을 모두 출력
if (length > 3) encode(sp, cp, length, dst);
else
for (i = 0; i < length; i++)
putc1(queue[qx(cp+i)], dst);
delete_all_jump(); /* jump_table[] 소거 */
fclose(dst);
}
이 알고리즘을 자세히 살펴보면 알겠지만 그 기본적인 틀은 Run-Length 압축법과 유사함을 알 수 있을 것이다. length 변수가 상태를 표시하고 있음이 특히 그렇다.
그리고 주의할 점은 jump_table[]에 위치를 기록하는 시점이다. 쉽게 생각하면 큐에 입력할 때 집어넣은 것으로 착각할 수
있기 때문이다. jump_tablel[]에 문자의 위치를 집어넣는 정확한 시점은 파일에 그 문자를 출력할 때이다.
그리고 큐 내에 일치하는 패턴이 두 개 이상 있을 때 어느 것이 우선적으로 선택되어야 하는가 하는 문제 또한 중요하다. 이 때
적절한 기준은 cp 쪽에 가까운 패턴을 취하는 것이다. 이렇게 하는 이유는 패턴이 cp에서 멀 경우 패턴의 다음 문자들까지도
일치할 수 있으나 sp의 앞부분이 큐에서 벗어나는 경우가 있기 때문에 압축을 중단해야 하는 경우가 생기기 때문이다.
이러한 점은 put_jump() 함수에서 자연스럽게 구현된다. put_jump() 함수는 항상 최근에 들어온 그 문자의 위치를 가장 앞에 두기 때문에 jump_table[]에서 검색할 때 퇴근에 들어온 문자의 위치가 선택된다.
마지막으로 Run-Length 압축법과 마찬가지로 Lempel-Ziv 압축법에서도 압축 정보의 표시를 위해 탈출
문자(Escape Character)를 사용한다. 그런데 이 탈출 문자가 문자 자체의 의미로 사용될 때 Run-Length에서는
<ESCAPE ESCAPE>쌍을 사용했지만, Lempel_Ziv 법은 <ESCAPE 0x00>쌍을
사용한다.
왜냐하면 탈출 문자가 사용되는 두 가지 용도는 문자 자체를 의미하는 것과 <탈출문자 상대위치 패턴길이> 정보의 시작을
표시하기 위함이다. 그런데 <상대위치>는 항상 0보다 큰 값이어야 하기 때문에(0이면 자기 자신을 의미한다) 압축
정보에서 <ESCAPE 0x00>쌍이 나타날 경우는 없다. 그러므로 충분히 압축 정보와 문자