ハフマン符号の理論と実装をわかりやすく解説

ハフマン符号(Huffman coding)は、1952年にデイヴィッド・ハフマンが考案した、最適な接頭辞符号を構成するアルゴリズムです。出現頻度の高いシンボルに短い符号語を、低いシンボルに長い符号語を割り当てることで、平均符号長を最小化します。

JPEG、MP3、ZIP などの圧縮フォーマットの内部で広く使われており、データ圧縮の最も基本的な手法の一つです。

本記事の内容

  • ハフマン符号の構成アルゴリズム
  • 最適性の証明(帰納法)
  • 平均符号長の上界 $H(X) \leq L < H(X) + 1$ の証明
  • シャノン=ファノ符号との比較
  • 拡大情報源とブロックハフマン符号
  • Pythonでのスクラッチ実装(ツリー構築・符号化・復号)
  • テキストデータでの圧縮率実験

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

ハフマン符号の構成アルゴリズム

基本的なアイデア

ハフマン符号の核となるアイデアは、確率の小さいシンボルほど長い符号語を割り当てるというものです。具体的には、ボトムアップに2分木(ハフマン木)を構築します。

アルゴリズム

$D = 2$(2元符号)の場合のアルゴリズムは以下の通りです。

入力: シンボル集合 $\mathcal{X} = \{x_1, x_2, \dots, x_m\}$ と確率分布 $p_1, p_2, \dots, p_m$

手順:

  1. 各シンボルを1つのノード(リーフ)として、確率をそのノードの重みとします。
  2. 最も確率の小さい2つのノードを選びます。
  3. この2つのノードを子として持つ新しい親ノードを作り、その確率を2つの子の確率の和とします。
  4. 元の2つのノードを集合から除き、新しい親ノードを追加します。
  5. ノードが1つになるまでステップ2〜4を繰り返します。
  6. 根から各リーフへの辺に 0/1 を割り当て、符号語とします。

具体例

$\mathcal{X} = \{A, B, C, D, E\}$, 確率 $(0.35, 0.25, 0.20, 0.12, 0.08)$ の場合を考えます。

ステップ1: 最小の2つ $D(0.12)$ と $E(0.08)$ を統合 → $DE(0.20)$

ノード 確率
A 0.35
B 0.25
C 0.20
DE 0.20

ステップ2: 最小の2つ $C(0.20)$ と $DE(0.20)$ を統合 → $CDE(0.40)$

ノード 確率
CDE 0.40
A 0.35
B 0.25

ステップ3: 最小の2つ $A(0.35)$ と $B(0.25)$ を統合 → $AB(0.60)$

ノード 確率
AB 0.60
CDE 0.40

ステップ4: $AB(0.60)$ と $CDE(0.40)$ を統合 → 根 $(1.00)$

得られるハフマン符号:

シンボル 確率 符号語 符号長
A 0.35 00 2
B 0.25 01 2
C 0.20 10 2
D 0.12 110 3
E 0.08 111 3

平均符号長: $L = 0.35 \times 2 + 0.25 \times 2 + 0.20 \times 2 + 0.12 \times 3 + 0.08 \times 3 = 2.20$ ビット

エントロピー: $H(X) = -(0.35\log_2 0.35 + 0.25\log_2 0.25 + 0.20\log_2 0.20 + 0.12\log_2 0.12 + 0.08\log_2 0.08) \approx 2.149$ ビット

冗長度: $L – H(X) = 2.20 – 2.149 = 0.051$ ビット(非常に小さい)

ハフマン符号の最適性の証明

定理

2元ハフマン符号は、すべての瞬時符号の中で平均符号長を最小化します。

証明の準備: 最適符号の性質

最適な瞬時符号は以下の性質を持つことを先に示します。$p_1 \geq p_2 \geq \cdots \geq p_m$ と仮定します。

補題1: 最適符号では、$p_i > p_j$ ならば $l_i \leq l_j$(確率が大きいシンボルほど符号語が短い)。

証明: $p_i > p_j$ かつ $l_i > l_j$ と仮定します。$x_i$ と $x_j$ の符号語を入れ替えると、平均符号長の変化は

$$ \begin{align} \Delta L &= p_i l_j + p_j l_i – (p_i l_i + p_j l_j) \\ &= (p_i – p_j)(l_j – l_i) < 0 \end{align} $$

$p_i – p_j > 0$ かつ $l_j – l_i < 0$ なので $\Delta L < 0$。つまり平均符号長が減り、元の符号は最適でなかったことになり矛盾。 $\square$

補題2: 最適符号では、最大符号語長のリーフは少なくとも2つ存在し、それらは兄弟(同じ親を持つ)にできます。

証明: もし最大深さのリーフが1つだけなら、その親ノードは子を1つしか持たないので、そのリーフを親の位置に移動すれば符号語長が1短くなり、平均符号長が減ります。これは最適性に矛盾します。

また、最大深さのリーフ2つが兄弟でない場合は、兄弟になるように入れ替えても平均符号長は変わりません(同じ深さの入れ替えなので)。 $\square$

最適性の証明(帰納法)

シンボル数 $m$ に関する帰納法で証明します。

基底: $m = 2$ のとき、符号語 $\{0, 1\}$ が唯一の瞬時符号(本質的に)であり、ハフマン符号はこれを出力します。明らかに最適です。

帰納ステップ: $m-1$ 個のシンボルに対してハフマン符号が最適であると仮定し、$m$ 個のシンボルに対して最適であることを示します。

$p_1 \geq p_2 \geq \cdots \geq p_m$ とします。補題1, 2 より、最適符号では確率最小の2つのシンボル $x_{m-1}$, $x_m$ が最大深さの兄弟であるとしてよいです。

ハフマンのアルゴリズムは $x_{m-1}$ と $x_m$ を統合して確率 $p_{m-1} + p_m$ の新しいシンボル $x’$ を作り、$m-1$ 個のシンボルに対するハフマン符号 $C’$ を構成します。

帰納法の仮定より、$C’$ は $m-1$ 個のシンボルの最適符号です。$C’$ での $x’$ の符号語長を $l’$ とします。

ハフマン符号 $C$ は、$x_{m-1}$ に $l’ + 1$、$x_m$ に $l’ + 1$ の符号語長を割り当てます。$C$ の平均符号長は

$$ L(C) = L(C’) – (p_{m-1} + p_m) l’ + p_{m-1}(l’ + 1) + p_m(l’ + 1) = L(C’) + p_{m-1} + p_m $$

任意の $m$ 個のシンボルの最適符号 $C^*$ を考えます。補題2より $x_{m-1}$ と $x_m$ が兄弟であるとしてよく、この2つを統合して $m-1$ 個のシンボルの符号 $C^{*\prime}$ を作ると、

$$ L(C^*) = L(C^{*\prime}) + p_{m-1} + p_m $$

帰納法の仮定より $L(C’) \leq L(C^{*\prime})$ なので、

$$ L(C) = L(C’) + p_{m-1} + p_m \leq L(C^{*\prime}) + p_{m-1} + p_m = L(C^*) $$

したがって $L(C) \leq L(C^*)$ であり、ハフマン符号 $C$ は最適です。 $\square$

平均符号長の上界の証明

定理

ハフマン符号の平均符号長 $L^*$ は

$$ \boxed{H(X) \leq L^* < H(X) + 1} $$

を満たします。

下界の証明

下界 $H(X) \leq L^*$ は情報源符号化定理(クラフトの不等式とKLダイバージェンスの非負性)から直ちに従います。ハフマン符号も瞬時符号なので、この下界が適用されます。

上界の証明

ハフマン符号は最適なので、任意の瞬時符号の平均符号長以下です。特に、シャノン符号(符号語長 $l_i = \lceil -\log_2 p_i \rceil$)の平均符号長以下です。

シャノン符号の平均符号長が $H(X) + 1$ 未満であることは前記事で示しました。

$$ L^* \leq L_{\text{Shannon}} < H(X) + 1 $$

したがって $L^* < H(X) + 1$ が成り立ちます。 $\square$

より精密な上界

実はハフマン符号については、より精密な上界が知られています。

$$ L^* \leq H(X) + p_{\max} $$

ここで $p_{\max} = \max_i p_i$ です。$p_{\max}$ が小さいとき、ハフマン符号はエントロピーにより近い平均符号長を達成します。

シャノン=ファノ符号との比較

シャノン=ファノ符号

シャノン=ファノ符号は、ハフマン符号が登場する前に用いられていた手法です。トップダウンにシンボルを2群に分割し、各群の確率の和ができるだけ等しくなるようにします。

アルゴリズム:

  1. シンボルを確率の大きい順にソートします。
  2. シンボルを2つのグループに分け、各グループの確率の合計がなるべく等しくなるようにします。
  3. 一方のグループに0を、他方に1を割り当てます。
  4. 各グループ内で、シンボルが1つになるまでステップ2〜3を再帰的に繰り返します。

比較

先ほどの例($p = (0.35, 0.25, 0.20, 0.12, 0.08)$)で比較します。

シンボル 確率 ハフマン符号 シャノン=ファノ符号
A 0.35 00 (2) 00 (2)
B 0.25 01 (2) 01 (2)
C 0.20 10 (2) 10 (2)
D 0.12 110 (3) 110 (3)
E 0.08 111 (3) 111 (3)

この例では同じ結果になりますが、一般にはハフマン符号のほうが平均符号長が短いか等しくなります。シャノン=ファノ符号は最適性が保証されません。

確率分布 $p = (0.40, 0.20, 0.20, 0.10, 0.10)$ の場合を考えると、違いが現れることがあります。

拡大情報源とブロックハフマン符号

拡大情報源

$n$ 個のシンボルをまとめて1つの「スーパーシンボル」とする情報源を$n$ 次拡大情報源と呼びます。

元の情報源が i.i.d. で確率分布 $p_1, \dots, p_m$ を持つ場合、$n$ 次拡大情報源のシンボルは $m^n$ 個あり、各スーパーシンボル $(x_{i_1}, \dots, x_{i_n})$ の確率は $\prod_{k=1}^{n} p_{i_k}$ です。

ブロックハフマン符号の利点

$n$ 次拡大情報源に対するハフマン符号の1シンボルあたりの平均符号長 $L_n / n$ は

$$ H(X) \leq \frac{L_n}{n} < H(X) + \frac{1}{n} $$

を満たします。$n$ を大きくすると、1シンボルあたりの平均符号長をエントロピーに任意に近づけることができます。

ただし、$n$ 次拡大情報源のアルファベットサイズは $m^n$ であり、$n$ が大きいと計算量・メモリが指数的に増加するという実用上の問題があります。

Pythonでのスクラッチ実装

ハフマン木のクラスとアルゴリズム

import heapq
from collections import Counter, defaultdict

class HuffmanNode:
    """ハフマン木のノード"""
    def __init__(self, char=None, freq=0, left=None, right=None):
        self.char = char    # リーフの場合のシンボル
        self.freq = freq    # 頻度(確率)
        self.left = left    # 左の子(0に対応)
        self.right = right  # 右の子(1に対応)

    def __lt__(self, other):
        # heapqでの比較用
        return self.freq < other.freq

    def is_leaf(self):
        return self.left is None and self.right is None


def build_huffman_tree(freq_dict):
    """頻度辞書からハフマン木を構築する"""
    # 各シンボルをリーフノードとしてヒープに追加
    heap = []
    for char, freq in freq_dict.items():
        heapq.heappush(heap, HuffmanNode(char=char, freq=freq))

    # ノードが1つになるまで統合を繰り返す
    while len(heap) > 1:
        # 最小の2ノードを取り出す
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)

        # 新しい親ノードを作成
        parent = HuffmanNode(
            freq=left.freq + right.freq,
            left=left,
            right=right
        )
        heapq.heappush(heap, parent)

    return heap[0]  # 根ノードを返す


def build_codebook(root):
    """ハフマン木から符号表を構築する"""
    codebook = {}

    def _traverse(node, code):
        if node.is_leaf():
            codebook[node.char] = code if code else '0'
            return
        if node.left:
            _traverse(node.left, code + '0')
        if node.right:
            _traverse(node.right, code + '1')

    _traverse(root, '')
    return codebook


def huffman_encode(text, codebook):
    """テキストをハフマン符号で符号化する"""
    return ''.join(codebook[ch] for ch in text)


def huffman_decode(encoded, root):
    """ハフマン符号を復号する"""
    decoded = []
    node = root
    for bit in encoded:
        if bit == '0':
            node = node.left
        else:
            node = node.right

        if node.is_leaf():
            decoded.append(node.char)
            node = root  # 根に戻る

    return ''.join(decoded)


# テスト
text = "ABRACADABRA"
print(f"元のテキスト: {text}")
print(f"テキスト長: {len(text)} 文字")

# 頻度を計算
freq = Counter(text)
print(f"頻度: {dict(freq)}")

# ハフマン木を構築
tree = build_huffman_tree(freq)

# 符号表を構築
codebook = build_codebook(tree)
print(f"符号表: {codebook}")

# 符号化
encoded = huffman_encode(text, codebook)
print(f"符号化結果: {encoded}")
print(f"符号化後のビット数: {len(encoded)}")

# 復号
decoded = huffman_decode(encoded, tree)
print(f"復号結果: {decoded}")
print(f"正しく復号できたか: {text == decoded}")

# 圧縮率の計算
original_bits = len(text) * 8  # ASCII: 1文字8ビット
compressed_bits = len(encoded)
print(f"\n固定長(ASCII): {original_bits} bits")
print(f"ハフマン符号: {compressed_bits} bits")
print(f"圧縮率: {compressed_bits / original_bits:.2%}")

# エントロピーとの比較
import numpy as np
probs = np.array(list(freq.values())) / len(text)
H = -np.sum(probs * np.log2(probs))
avg_len = len(encoded) / len(text)
print(f"\nエントロピー H(X): {H:.4f} bits/symbol")
print(f"平均符号長: {avg_len:.4f} bits/symbol")
print(f"冗長度: {avg_len - H:.4f} bits/symbol")

テキストデータでの圧縮率実験

import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
import heapq

class HuffmanNode:
    """ハフマン木のノード"""
    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

    def is_leaf(self):
        return self.left is None and self.right is None

def build_huffman_tree(freq_dict):
    """ハフマン木を構築"""
    heap = [HuffmanNode(char=c, freq=f) for c, f in freq_dict.items()]
    heapq.heapify(heap)
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        parent = HuffmanNode(freq=left.freq + right.freq, left=left, right=right)
        heapq.heappush(heap, parent)
    return heap[0]

def build_codebook(root):
    """符号表を構築"""
    codebook = {}
    def _traverse(node, code):
        if node.is_leaf():
            codebook[node.char] = code if code else '0'
            return
        if node.left:
            _traverse(node.left, code + '0')
        if node.right:
            _traverse(node.right, code + '1')
    _traverse(root, '')
    return codebook

def analyze_compression(text, label):
    """テキストのハフマン符号化を分析"""
    freq = Counter(text)
    n = len(text)
    probs = np.array([freq[c] / n for c in freq])

    # エントロピー
    H = -np.sum(probs * np.log2(probs))

    # ハフマン符号を構築
    tree = build_huffman_tree(freq)
    codebook = build_codebook(tree)

    # 平均符号長
    avg_len = sum(freq[c] * len(codebook[c]) for c in freq) / n

    # 固定長符号の符号長
    fixed_len = np.ceil(np.log2(len(freq)))

    return {
        'label': label,
        'n_chars': n,
        'n_unique': len(freq),
        'entropy': H,
        'avg_huffman': avg_len,
        'fixed_len': fixed_len,
        'compression_ratio': avg_len / 8,  # vs ASCII
        'redundancy': avg_len - H,
        'codebook': codebook
    }

# さまざまなテキストでテスト
texts = {
    'English-like': 'the quick brown fox jumps over the lazy dog ' * 20,
    'Repetitive': 'aaaaaabbbbccd' * 50,
    'Uniform': ''.join([chr(65 + i % 26) for i in range(500)]),
    'Binary': '0110100110010110' * 40,
    'Japanese-sim': 'あいうえおかきくけこさしすせそ' * 30,
}

results = []
for label, text in texts.items():
    result = analyze_compression(text, label)
    results.append(result)
    print(f"\n--- {label} ---")
    print(f"  文字数: {result['n_chars']}, 種類数: {result['n_unique']}")
    print(f"  エントロピー: {result['entropy']:.4f} bits/symbol")
    print(f"  ハフマン平均符号長: {result['avg_huffman']:.4f} bits/symbol")
    print(f"  冗長度: {result['redundancy']:.4f} bits/symbol")
    print(f"  圧縮率 (vs 8-bit): {result['compression_ratio']:.2%}")

# 可視化: エントロピーと平均符号長の比較
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左: 各テキストのエントロピーと平均符号長
ax = axes[0]
labels = [r['label'] for r in results]
entropies = [r['entropy'] for r in results]
avg_lens = [r['avg_huffman'] for r in results]
fixed_lens = [r['fixed_len'] for r in results]

x = np.arange(len(labels))
width = 0.25
ax.bar(x - width, entropies, width, label='H(X) (entropy)', color='steelblue')
ax.bar(x, avg_lens, width, label='Huffman avg length', color='salmon')
ax.bar(x + width, fixed_lens, width, label='Fixed-length', color='lightgreen')
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=15, ha='right')
ax.set_ylabel('Bits per symbol', fontsize=12)
ax.set_title('Entropy vs Average codeword length', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3, axis='y')

# 右: 冗長度の比較
ax = axes[1]
redundancies = [r['redundancy'] for r in results]
colors = ['steelblue' if r < 0.5 else 'salmon' for r in redundancies]
ax.barh(labels, redundancies, color=colors, edgecolor='black', linewidth=0.5)
ax.set_xlabel('Redundancy (bits/symbol)', fontsize=12)
ax.set_title('Huffman code redundancy = L - H(X)', fontsize=13)
ax.grid(True, alpha=0.3, axis='x')

plt.tight_layout()
plt.show()

ハフマン木の可視化

import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
import heapq

class HuffmanNode:
    """ハフマン木のノード"""
    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
    def is_leaf(self):
        return self.left is None and self.right is None

def build_huffman_tree(freq_dict):
    """ハフマン木を構築"""
    heap = [HuffmanNode(char=c, freq=f) for c, f in freq_dict.items()]
    heapq.heapify(heap)
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        parent = HuffmanNode(freq=left.freq + right.freq, left=left, right=right)
        heapq.heappush(heap, parent)
    return heap[0]

def compute_positions(node, x=0, y=0, dx=2.0, positions=None):
    """木の各ノードの描画位置を計算"""
    if positions is None:
        positions = {}
    pos_id = id(node)
    positions[pos_id] = (x, y, node)

    if node.left:
        compute_positions(node.left, x - dx, y - 1, dx / 2, positions)
    if node.right:
        compute_positions(node.right, x + dx, y - 1, dx / 2, positions)

    return positions

# テスト用のデータ
text = "ABRACADABRA"
freq = Counter(text)

# ハフマン木を構築
tree = build_huffman_tree(freq)

# 位置を計算
positions = compute_positions(tree, dx=3.0)

# 可視化
fig, ax = plt.subplots(figsize=(12, 8))

# エッジと0/1ラベルを描画
def draw_tree(node, x, y, dx, ax):
    if node.left:
        lx, ly = x - dx, y - 1
        ax.plot([x, lx], [y, ly], 'k-', linewidth=1.5)
        ax.text((x + lx) / 2 - 0.15, (y + ly) / 2 + 0.1, '0',
                fontsize=12, color='blue', fontweight='bold')
        draw_tree(node.left, lx, ly, dx / 2, ax)
    if node.right:
        rx, ry = x + dx, y - 1
        ax.plot([x, rx], [y, ry], 'k-', linewidth=1.5)
        ax.text((x + rx) / 2 + 0.15, (y + ry) / 2 + 0.1, '1',
                fontsize=12, color='red', fontweight='bold')
        draw_tree(node.right, rx, ry, dx / 2, ax)

draw_tree(tree, 0, 0, 3.0, ax)

# ノードを描画
for pos_id, (x, y, node) in positions.items():
    if node.is_leaf():
        circle = plt.Circle((x, y), 0.3, color='lightgreen',
                             ec='black', linewidth=1.5, zorder=5)
        ax.add_patch(circle)
        ax.text(x, y, f"{node.char}\n({node.freq})", ha='center', va='center',
                fontsize=10, fontweight='bold', zorder=6)
    else:
        circle = plt.Circle((x, y), 0.3, color='lightyellow',
                             ec='black', linewidth=1.5, zorder=5)
        ax.add_patch(circle)
        ax.text(x, y, f"{node.freq}", ha='center', va='center',
                fontsize=10, zorder=6)

ax.set_xlim(-5, 5)
ax.set_ylim(-5, 1)
ax.set_aspect('equal')
ax.set_title(f'Huffman Tree for "{text}"', fontsize=14)
ax.axis('off')
plt.tight_layout()
plt.show()

ブロックハフマン符号の効果

import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from itertools import product
import heapq

class HuffmanNode:
    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
    def is_leaf(self):
        return self.left is None and self.right is None

def build_huffman_tree(freq_dict):
    heap = [HuffmanNode(char=c, freq=f) for c, f in freq_dict.items()]
    heapq.heapify(heap)
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        parent = HuffmanNode(freq=left.freq + right.freq, left=left, right=right)
        heapq.heappush(heap, parent)
    return heap[0]

def build_codebook(root):
    codebook = {}
    def _traverse(node, code):
        if node.is_leaf():
            codebook[node.char] = code if code else '0'
            return
        if node.left:
            _traverse(node.left, code + '0')
        if node.right:
            _traverse(node.right, code + '1')
    _traverse(root, '')
    return codebook

# 情報源の定義
symbols = ['A', 'B', 'C']
probs = [0.7, 0.2, 0.1]
H = -sum(p * np.log2(p) for p in probs)

# ブロックサイズごとの平均符号長を計算
block_sizes = range(1, 7)
avg_lengths = []

for k in block_sizes:
    # k次拡大情報源の確率分布を計算
    block_freq = {}
    for block in product(range(len(symbols)), repeat=k):
        p_block = np.prod([probs[s] for s in block])
        block_name = ''.join([symbols[s] for s in block])
        block_freq[block_name] = p_block

    # ハフマン木を構築
    tree = build_huffman_tree(block_freq)
    codebook = build_codebook(tree)

    # 1シンボルあたりの平均符号長
    avg_len = sum(block_freq[b] * len(codebook[b]) for b in block_freq) / k
    avg_lengths.append(avg_len)
    print(f"ブロックサイズ k={k}: L/k = {avg_len:.4f} bits/symbol "
          f"(冗長度 = {avg_len - H:.4f})")

print(f"\nエントロピー H(X) = {H:.4f} bits/symbol")

# 可視化
fig, ax = plt.subplots(figsize=(10, 6))

ax.plot(list(block_sizes), avg_lengths, 'bo-', linewidth=2, markersize=8,
        label='Block Huffman L/k')
ax.axhline(H, color='red', linewidth=2, linestyle='--',
           label=f'H(X) = {H:.4f}')

# 理論上界
theory_upper = [H + 1/k for k in block_sizes]
ax.plot(list(block_sizes), theory_upper, 'g--', linewidth=1.5,
        label='H(X) + 1/k (upper bound)')

ax.fill_between(list(block_sizes), H, theory_upper,
                alpha=0.1, color='green')

ax.set_xlabel('Block size k', fontsize=12)
ax.set_ylabel('Average bits per symbol', fontsize=12)
ax.set_title('Block Huffman coding: convergence to entropy', fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_xticks(list(block_sizes))

plt.tight_layout()
plt.show()

まとめ

本記事では、ハフマン符号の理論と実装について解説しました。

  • 構成アルゴリズム: 確率最小の2ノードを繰り返し統合してボトムアップに2分木を構築
  • 最適性: 帰納法により、ハフマン符号がすべての瞬時符号の中で最小の平均符号長を達成することを証明
  • 平均符号長の上界: $H(X) \leq L^* < H(X) + 1$
  • ブロックハフマン符号: ブロックサイズ $k$ を大きくすると $L/k \to H(X)$ に収束
  • Pythonでの実装: ハフマン木の構築、符号化、復号をスクラッチで実装

ハフマン符号は最適な瞬時符号ですが、シンボルの独立同一分布を仮定しています。実際のデータでは、算術符号(arithmetic coding)やLempel-Ziv符号(LZ77/LZ78)など、系列の相関を利用するアルゴリズムがより高い圧縮率を達成します。

次のステップとして、以下の記事も参考にしてください。