관련글 :
| 그리디 | https://waydd.tistory.com/91 |
| 활동 선택 문제 | https://waydd.tistory.com/111 |
1. 허프만 코딩 (Huffman Coding)
주어진 문자들의 빈도에 따라 최소 길이의 이진 코드를 만드는/할당하는 그리디 알고리즘, 트리 기반의 압축 알고리즘(이자 문제 이름)
→ 그리디 알고리즘의 대표 예시로, 매 단계에서 가장 비용이 적은 두 항목을 선택하여 병합
→ 트리 구조 생성 + 그 트리 바탕으로 이진 코드 부여
- 자주 나오는 문자는 짧게, 드물게 나오는 문자는 길게 빈도 기반 인코딩
→ 빈도 수 큰 문자가 많은 비중을 차지, 짧게 표현함으로서 전체 평균 길이 (혹은 전체 데이터 크기) 를 최소화
(무손실 압축이므로 나중에 복원도 가능)
※ 인코딩이란? 다른 환경에 해석 가능하도록 데이터를 특정 형식으로 바꿈 (원래 정보 보존 + 저장, 전송, 압축, 보안 등)
→ 가변 길이 이진 인코딩 (문자마다 길이가 다른 이진수 코드로 바꾸는 인코딩 방식) - 우선순위 큐(PriorityQueue), 최소 힙 사용
- 빈도 낮은 것부터 병합해 최소 비용 트리 구성 (최소 이진트리)
① 각 문자 빈도를 계산 후, 빈도 낮은 것끼리(최소 두개) 합쳐서 트리 생성 (최소 힙 구조에 활용)
(전체 비트 수의 총합이 최소가 되도록 트리를 구성함)
② 이후 루트에서 왼쪽 = 0, 오른쪽 = 1 로 경로를 따라가며 이진 코드 생성
③ 각 문자에 겹치지 않는 (prefix-free) 코드 할당 으로 인코딩 완성
→ 압축(최소한의 비트로 표현)을 통해 최단 비트 수 ( = 데이터를 표현하는데 필요한 전체 비트 길이를 가장 작게 만듦 )
→ 실제 문자는 리프 노드에 존재, 비트 경로 = 리프 노드의 문자
(이때 부모 노드의 빈도는 두 자식 노드의 빈도 합)
→ 결과 자체에는 빈도수 정보는 들어있지 않음, 전체 트리가 정렬 트리는 아니지만 경로 길이가 짧을 수록 빈도는 적음(비례)
(트리 좌우 위치 : 빈도 기준 X / 비트 코드 : 사전순 정렬 X, 길이순 정렬 X) - 허프만 코딩 ≈ 허프만 인코딩
→ 다 같은 알고리즘에서 비롯, 코딩은 알고리즘, 인코딩은 그 결과나 행위/과정 을 강조 - 파일 압축 (JPEG, MP3, ZIP 등) 에 사용됨
- 로프 연결 / 병합 최소 비용 / 압축 / 이진 트리 응용 문제로 간접적으로 출제 (트리나 PQ 활용 문제로 변형됨)
※ 인코딩의 반대는 다시 복원하는 복호화 (Decoding)
※ 대부분의 인코딩은 원래대로 되돌릴 수 있어야(가역성) 함 (비손실/무손실 압축) ex) Base64, 허프만 코딩
※ prefix-free 만족이란?
- 아무 코드도 다른 코드의 앞부분이 아님을 뜻함
- 예시) 0 은 10, 110 의 시작이 아님 등
- 다른 코드의 접두어가 되는 코드가 있으면 디코딩할 수 없음
- 허프만 코디은 트리 구조를 통해 prefix-free 조건을 자동 보장
※ 허프만 트리 구성 단계 설명 (① ~ ④, 위에도 설명 있음)
① 빈도수 계산
→ 각 문자의 등장 횟수를 계산 ex) A(7), B(3), C(2), D(1)
② 우선순위 큐 생성
→ 각 문자를 노드로 만들고 최소 힙(min-heap)에 삽입
③ 트리 병합 반복
→ 최소 빈도수 2개를 꺼내 새로운 부모 노드로 병합(합침) + 다시 큐에 삽입 (최소 힙 구조)
④ 최종 트리(= 하프만 트리) 생성
→ 노드가 하나 남을 때까지 반복, 최종 루트 노드 완성
⑤ 코드 매핑 생성
→ 루트에서 리프까지 경로에 따라가며 문자 ↔ 이진 코드 생성
⑥ 원문 인코딩
- 용어 요약
| 허프만 트리 (Huffman Tree) | 문자 + 빈도수로 구성된 이진 트리 |
| 병합 단계(Merge step) | 최소 노드 2개를 꺼내 새로운 노드로 합치는 과정 |
| 우선순위 큐 (min-heap) | 가장 낮은 빈도를 빠르게 선택하기 위한 자료구조 |
| 리프 노드 | 실제 문자가 위치하는 최종 노드 |
| 내부 노드 | 합쳐진 노드로, 빈도수는 자식 합, 문자는 없음 |
- 인코딩 예시
- 원래 문자열 : AAAAAAABBBCCD
→ 고정 길이 인코딩 (ASCII 8비트) : 13(총 문자수) × 8 = 104비트 (허프만 아님) - 허프만 코딩 매핑 (압축할 때만 사용) : A : 0, B : 10, C : 110, D : 111
- 압축 결과 (인코딩 결과) : 00000001010101110110111
→ 인코딩 후 전체 비트 수 = 7×1 + 3×2 + 2×3 + 1×3 = 7 + 6 + 6 + 3 = 22비트 (총합 기준 최적화)
※ 압축률: 22 / 104 ≈ 21.2% (가변 길이 인코딩 중 최적)
※ 허프만 트리 구성 ( 우선순위 큐 기반 병합 단계 )
(*)
/ \
A(7) (*)
/ \
B(3) (*)
/ \
C(2) D(1)
※ 위의 병합 순서는 아래와 같음
① C(2) + D(1) → CD(3)
② CD(3) + B(3) → BCD(6)
③ A(7) + BCD(6) → 루트(13)
※ 이진 코드 매핑표 생성 (빈도 기반 비트)
- 루트에서 왼쪽은 0, 오른쪽은 1, 문자마다 빈도수에 따라 짧고 긴 이진코드를 부여
→ 매핑 자체를 허프만 인코딩이라고 함 (= 어떤 문자에 어떤 비트 코드가 할당되었는가)
→ 이후, 매핑된 코드로 실제 데이터를 압축한 결과인 전체 비트열도, 허프만 인코딩
- A : 루트 → 왼쪽 = 0
- B : 루트 → 오른쪽 → 왼쪽 = 10
- C : 루트 → 오른쪽 → 오른쪽 → 왼쪽 = 110
- D : 루트 → 오른쪽 → 오른쪽 → 오른쪽 = 111
→ 문자는 항상 리프에만 위치, 내부 노드는 오직 빈도수 합산 역할만
※ 인코딩 결과 생성
00000001010101110110111
- A × 7 : 0 0 0 0 0 0 0 → 0000000
- B × 3 : 10 10 10 → 101010
- C × 2 : 110 110 → 110110
- D × 1 : 111 → 111
- 한계 및 주의점
- 이론적으로 최적에 가까운 무손실 압축률 + 구현이 비교적 단순 + 정적 데이터에 매우 효과적
- 문자 확률이 2의 제곱수에 가까울 때 최적
→ 이진 트리 구조로 인함 - 모든 입력에 대해 최적인 것은 아님
→ 빈도수에 따라 만들어진 트리 (또는 코드 테이블) 는 입력 데이터 전체에 고정적으로 사용
(압축 및 복호화 속도가 빠름 + 구현 단순)
→ 데이터 특성이 구간별로 변할 때 비효율적임 ex) 문서의 앞부분은 영어, 뒷부분은 숫자가 많을 - 동적 상황(데이터가 계속 변할 때) 에서는 부적합
→ 데이터를 모두 분석 후, 전체 빈도수를 알아야 트리를 만들 수 있음 ( = 전체 입력을 미리 알아야 압축이 가능 )
→ 때문에 스트리밍/실시간 에 부적절 - 압축과 함께 빈도 정보, 트리 구조 또는 코드 매핑 정보도 저장해야 함
→ 복호화(디코딩)를 위해 코드 테이블 필요
2. 대표 예제 설명
- 구현 과정 및 설명
| 핵심 기술 | 설명 | |
| 빈도 분석 | - 우선순위 큐 사용 (heapq 사용) - 클래스 / 튜플 우선순위 정의 |
- 최소 힙으로 트리를 구성해야 함 - Node(frequency, ...) 객체 정의 |
| 트리 생성 | 트리 병합 시뮬레이션 (최소 힙 기반 병합) | 자식 노드 병합 및 새 노드 생성 구조 |
| 이진(비트) 코드 매핑 | 재귀 DFS | 문자별 코드 매핑 생성 |
| 인코딩/디코딩 | 딕셔너리 활용 | har → code, code → char 변환 |
※ 디코딩(복호화) 방식
- 이진 문자열을 앞에서부터 한 비트씩 따라감
- 트리의 왼쪽 또는 오른쪽으로 이동, 리프 노드에 도달하면 해당 문자를 출력, 다시 루트로 돌아가 다음 경로를 따라감
※ 디코딩 시 필요한 정보
- 허프만 트리 자체 또는 문자/비트 매핑 테이블 필요, 이중 하나를 함께 저장하거나 전송
- 아래 코드는 허프만 트리 자체를 넘겨줌 (root)
- 과정별 시간 복잡도
- 허프만 트리 생성 : O(n log n)
→ n은 서로 다른 문자 수 (보통 알파벳이면 26~128 정도) - 전체 압축도 포함 : O(N + n log n) (거의 선형)
→ 여기서 N은 전체 문자열 길이 - 실제 압축 시, 전체 텍스트를 순회하며 각 문자마다 매핑된 코드로 변환 → 인코딩: O(N)
- 복호화 시에는 트리를 따라 내려가므로 O(N)
| 설명 | 시간 복잡도 | |
| 빈도수 계산 | 문자열 한 번 순회 | O(N) (문자열 길이) |
| 최소 힙 초기화 | 힙에 n개 원소 삽입 | O(n) (heapify) |
| 트리 생성 | 힙에서 2개 pop + 삽입 반복 → n-1회 | O(n log n) |
| 코드 할당 (인코딩/디코딩) |
트리 순회하며 각 문자에 이진 코드 부여 | O(n) (DFS or BFS) |
- 코드 구현
import heapq
from collections import defaultdict, Counter
# 트리 노드 클래스 정의
class Node:
def __init__(self, char=None, freq=0, left=None, right=None):
self.char = char # 문자 (리프 노드만 가짐)
self.freq = freq # 빈도수
self.left = left
self.right = right
def __lt__(self, other):
return self.freq < other.freq # heapq에서 빈도 비교
# 허프만 트리 생성 함수
def build_huffman_tree(text):
freq_map = Counter(text)
heap = [Node(char=c, freq=f) for c, f in freq_map.items()]
heapq.heapify(heap)
while len(heap) > 1:
a = heapq.heappop(heap)
b = heapq.heappop(heap)
merged = Node(freq=a.freq + b.freq, left=a, right=b)
heapq.heappush(heap, merged)
return heap[0] # 루트 노드
# 문자별 이진 코드 생성
def generate_codes(node, current_code="", code_map=None):
if code_map is None:
code_map = dict()
if node.char is not None:
code_map[node.char] = current_code
else:
generate_codes(node.left, current_code + "0", code_map)
generate_codes(node.right, current_code + "1", code_map)
return code_map
# 인코딩 함수
def huffman_encode(text):
root = build_huffman_tree(text)
code_map = generate_codes(root)
encoded_text = ''.join(code_map[char] for char in text)
return encoded_text, code_map, root
# 디코딩 함수
def huffman_decode(encoded_text, root):
decoded = []
node = root
for bit in encoded_text:
node = node.left if bit == '0' else node.right
if node.char is not None:
decoded.append(node.char)
node = root
return ''.join(decoded)
# 테스트
text = "AAAAAAABBBCCD"
encoded, code_map, root = huffman_encode(text)
decoded = huffman_decode(encoded, root)
print("원문:", text)
print("인코딩 결과:", encoded)
print("문자별 코드맵:", code_map)
print("디코딩 결과:", decoded)
3. 다른 압축 알고리즘과 비교
- 압축 알고리즘
- 크게 무손실 압축, 손실 압축(MP3, JPEG 등) 으로 나눔
- 무손실 압축 에는 통계 기반, 사전 기반(LZ77, LZ78), 반복 기반
- 통계 기반에는 허프만 코딩과 산술 코딩(Arithmetic Coding)
- 데이터 분석으로 정보량을 줄이는 건 아니지만, 같은 문자가 연속해서 나올 확률이 높아지는 구조 (패턴을 뭉침) 로 바꿔주는 BWT(회전 배열 → 사전 정렬 → 마지막 열 추출) 전처리 알고리즘(변환 알고리즘) 과 조합해서 사용
- 실제 압축 도구에서는 보완적으로 결합해서 사용 ex) ZIP 파일은 LZ77 + 허프만 (DEFLATE)
| 허프만 코딩 | LZW (Lempel–Ziv–Welch) | RLE (Run-Length Encoding) | |
| 압축 방식 | 빈도 기반 가변 길이 인코딩 | 사전 기반 문자열 치환 | 반복 문자 압축 |
| 원리 | 자주 나오는 문자는 짧게, 드문 문자는 길게 인코딩 | 문자열 패턴 → 사전에서 코드로 치환 | 연속된 동일 문자 → 문자 + 횟수로 치환 |
| 압축 단위 | 문자 단위 (Symbol) | 문자열 (패턴) | 연속된 동일 문자 |
| 대표 장점 | 평균 비트 수 최소화 | 반복되는 패턴에 효과적, 실시간 압축 가능 | 매우 단순, 속도 빠름 |
| 단점 | 트리 저장 필요, 실시간 처리 어려움 | 초기 사전 크기 제약, 복잡도 있음 | 반복 없으면 오히려 커짐 |
| 실전 예시 | ZIP, JPEG, MP3 내부 | GIF 파일 압축, Unix compress | 흑백 이미지, 단순 텍스트 |
- 압축 효율 예시
| 허프만 코딩 | LZW | RLE | |
| aaaaaaa | 적당히 좋음 | 거의 동일 문자열이므로 적당히 좋음 | 매우 효율적 |
| abababab | 좋지 않음 | 효율적 (패턴 반복) | 거의 차이 없음 |
| abcdefg | 별 차이 없음 | 비효율 (패턴 없음) | 오히려 비효율 |
- 허프만의 위치적 특성 요약
| 역할 | 문자 빈도 기반 최적 압축 기법 |
| 강점 | 평균 코드 길이 최적화 (정보이론 기반), 고정된 코드 성능 보장 |
| 약점 | 패턴, 반복, 실시간 처리에는 약함 |
| 보완 | 다른 알고리즘과 결합되어 사용 (ex. BWT → MTF → RLE → 허프만) |
- 산술 코딩 (Arithmetic Coding)
- 하나의 실수로 전체 메시지를 압축하는 고정밀 통계 기반 압축 알고리즘
- 압축률이 매우 뛰어남
→ 허프만은 문자 단위인 것에 비해 산술 코딩은 전체 메시지 단위
→ 각 문자마다 고정된 코드가 아닌 전체를 하나의 실수로 표현 + 접두사 조건 없음
→ 때문에 허프만보다 같거나 더 좋음 - But. 부동소수점 정밀도 문제 + 구현 복잡도 로 인해 일반적인 경우, 잘 쓰지 X
- 고급 압축 포맷이나 연구용 모델에서 주로 활용
| 정의 | 문자열 전체를 하나의 실수 구간 [0, 1) 안의 부구간(subinterval)으로 표현하여 압축하는 방식 |
| 압축 방식 | 전체 메시지를 하나의 부동소수점 값으로 인코딩함 |
| 동작 원리 | 각 문자의 등장 확률에 따라 [0,1) 구간을 분할하고, 입력 문자열을 읽으며 그 구간을 점점 좁혀나감 |
| 예시 (과정 요약) | ① 문자별 확률 계산 ② [0,1) 구간을 확률에 따라 나눔 ③ 각 문자를 읽을 때마다 해당 구간으로 좁힘 ④ 마지막 구간 내의 실수 하나를 출력 |
| 압축 결과 | 최종적으로 특정 실수 하나를 이진수로 표현함 → 이 값 하나로 전체 문자열을 복원 가능 |
| 장점 | - 압축률이 매우 높고 정밀함 - 문자 간 확률 차이가 적어도 효율 유지 - 부가 정보가 적음 (코드 테이블 불필요) |
| 단점 | - 부동소수점 오차 문제 존재 - 정밀한 연산이 필요하여 속도와 구현 난이도가 높음 - 정수 기반 재작성(Int-based version) 필요함 |
| 구현 방식 | - 부동소수점 또는 정수 기반 산술 코딩 - 상한/하한을 반복적으로 계산하며 구간을 축소함 |
| 복호화 방식 | 동일한 확률 분포표를 이용해 실수 값을 다시 문자로 되돌리는 방식→ 실수값을 통해 문자 구간을 역추적 |
| 사용 예시 | - JPEG2000 - H.264 - bzip2의 일부 변형 - 고정밀 압축 도구(PAQ, CM 등) |
| 사용 목적 | - 비트 단위까지 압축 효율을 극대화하고자 할 때 - 확률 모델 기반 통계 압축을 더 정밀하게 수행할 때 |
'CS > Algorithms' 카테고리의 다른 글
| ♟ 빠르고 똑똑하게 길찾기, A* 알고리즘 (+ 휴리스틱) (0) | 2025.07.28 |
|---|---|
| ♟ 가장 많은 일정을 소화하는 법, 활동 선택(Activity Selection) (0) | 2025.07.28 |
| ♟ 구간을 유지하며 처리하는 전략, 슬라이딩 윈도우 (0) | 2025.07.21 |
| ♟ 위상 정렬 (0) | 2025.07.09 |
| ♟ 단순/강력, 모든 경우의 수를 살펴보는 완전탐색(Brute-force) (0) | 2025.07.07 |