LZ 编码
基础知识
LZ编码(Lempel-Ziv Code)巧妙地将字典技术应用于通用数据压缩领域,而且可以从理论上证明LZ系列算法可以逼近信息熵的极限。
设信源符号集 A = { a 1 , a 2 , . . . , a K } A=\{a_1,a_2,...,a_K\} A={a1,a2,...,aK}共 K K K个符号,设输入信源符号序列为 u = ( u 1 , u 2 , . . . , u L ) u=(u_1,u_2,...,u_L) u=(u1,u2,...,uL);编码时将此序列分成不同的段。分段规则为:尽可能取最少个相连的信源符号,保证各段都不同。
开始时,先取一个符号作为第一段,然后继续分段。若出现与前面相同的符号时,就再取紧跟后面的一个符号一起组成一个段,使之与前面的段不同。这些分段构成字典。
编码的码字由段号加后面一个符号组成。设 u u u构成的字典中的短语共有 M ( u ) M(u) M(u)个。若编为二元码,段号所需码长 n = ⌈ log M ( u ) ⌉ n=\lceil \log M(u)\rceil n=⌈logM(u)⌉,每个符号需要的码长为 ⌈ log K ⌉ \lceil \log K\rceil ⌈logK⌉,单符号的码字段号为 0 0 0。
例子
设 U = { a 1 , a 2 , a 3 , a 4 } U=\{a_1,a_2,a_3,a_4\} U={a1,a2,a3,a4},信源符号为 a 1 a 2 a 1 a 3 a 2 a 4 a 2 a 4 a 3 a 1 a 1 a 4 a_1a_2a_1a_3a_2a_4a_2a_4a_3a_1a_1a_4 a1a2a1a3a2a4a2a4a3a1a1a4;
首先一共有4个符号,那么每个符号需要的码长为 log 4 = 2 \log 4=2 log4=2;
即 a 1 , a 2 , a 3 , a 4 a_1,a_2,a_3,a_4 a1,a2,a3,a4 可以分别编为 00 , 01 , 10 , 11 00,01,10,11 00,01,10,11;
然后按照分段规则,可以分为: a 1 , a 2 , a 1 a 3 , a 2 a 4 , a 2 a 4 a 3 , a 1 a 1 , a 4 a_1,a_2,a_1a_3,a_2a_4,a_2a_4a_3,a_1a_1,a_4 a1,a2,a1a3,a2a4,a2a4a3,a1a1,a4 这7段;
那么段号需要的码长为 ⌈ log 7 ⌉ = 3 \lceil \log 7\rceil=3 ⌈log7⌉=3;所以一个短语需要使用5个比特;
然后开始编码(注意段号从1开始,单字符段号为0)
a 1 a_1 a1为单字符,段号为0,编为 00000 00000 00000;
a 2 a_2 a2为单字符,段号为0,编为 00001 00001 00001;
a 1 a 3 a_1a_3 a1a3,前缀 a 1 a_1 a1段号为1,字符 a 3 a_3 a3,编为 00110 00110 00110;
a 2 a 4 a_2a_4 a2a4,前缀 a 2 a_2 a2段号为2,字符 a 4 a_4 a4,编为 01011 01011 01011;
a 2 a 4 a 3 a_2a_4a_3 a2a4a3,前缀 a 2 a 4 a_2a_4 a2a4段号为 4 4 4,字符 a 3 a_3 a3,编为 10010 10010 10010;
a 1 a 1 a_1a_1 a1a1,前缀 a 1 a_1 a1段号为 1 1 1,字符 a 1 a_1 a1,编为 00100 00100 00100;
a 4 a_4 a4为单字符,段号为 0 0 0,编为 00011 00011 00011;
那么最终编码结果为 00000000010011001011100100010000011 00000000010011001011100100010000011 00000000010011001011100100010000011。
实现
编码:先根据字符集确定符号码长 n 1 n_1 n1,对每个字符先编码;然后扫描符号序列,创建好字典(为了方便后续编码,可以先记录下所有短语的位置)确定段号码长 n 2 n_2 n2;再对所有短语进行编码为段号+字符编码;最后返回编码结果、字符全集、字典长度方便译码。
译码:同样先确定短语码长 n 1 + n 2 n_1+n_2 n1+n2;然后每次截取一个短语的编码,得到段号和字符,译为对应的短语;并且一边译码一遍建立字典(所以并不需要传输字典本身)
from math import log2, ceil
def LZ_Compress(message): # LZ编码,先构建完字典并记录短语位置;再分别对每个短语编码(段号+字符编码)
U = sorted(list(set(message))) # 字符全集U,按字典序来排列
n1 = ceil(log2(len(U))) # 每个符号需要的码长,log向上取整
char = {U[i]: bin(i)[2:].zfill(n1) for i in range(len(U))} # 对单个符号编码
dict = {} # 短语字典
word_index = [] # 在创建字典过程中,记录编码短语的位置
i = 0
count = 1 # 段号,从1开始
while i < len(message):
for j in range(1, len(message) - i + 1):
word = message[i:i + j] # 截取一个短语
if word not in dict: # 不在字典,则添加进去
dict[word] = count
word_index.append((i, i + j)) # 记录下每个短语的位置
count += 1 # 段号+1
i = i + j # 更新位置
break
else:
i += 1
n2 = ceil(log2(len(dict))) # 段号需要的码长,log向上取整
message_code = '' # 最终编码结果
for ind in word_index:
i, j = ind
if i + 1 == j: # 短语长度为1
message_code = message_code + '0' * n2 + char[message[i]] # 段号+字符编码 段号为0
else:
message_code = message_code + bin(dict[message[i:j - 1]])[2:].zfill(n2) + char[message[j - 1]] # 段号+字符编码
return U, len(dict), message_code # 返回字符全集,字典长度,编码结果;方便译码
def LZ_UnCompress(U, dict_len, code): # 根据编码规则,边译码边重构字典(只需字典长度用来确定段号,以及字符全集即可)
n1 = ceil(log2(len(U))) # 字符码长
n2 = ceil(log2(dict_len)) # 段号码长
chars = {bin(i)[2:].zfill(n1): U[i] for i in range(len(U))} # 字符编码集
dict = {} # 字典
message = '' # 最终译码结果
count_ = 1 # 段号
for i in range(0, len(code), n1 + n2): # 每次截取一个短语编码的结果
count = code[i:i + n2] # 取出段号
char = code[i + n2:i + n2 + n1] # 取出字符编码
if int(count, 2) == 0: # 段号为0,直接译为单个字符
decode_word = chars[char]
message += decode_word
dict[bin(count_)[2:].zfill(n2)] = decode_word # 添加进字典
else:
decode_word = dict[count] + chars[char] # 否则译为前缀短语+字符
message = message + decode_word
dict[bin(count_)[2:].zfill(n2)] = decode_word # 添加进字典
count_ += 1 # 段号自增
return message
message = 'abacbdbdcaad'
U, n2, message_code = LZ_Compress(message) # 编码
print(message_code)
# 00000000010011001011100100010000011
message_ = LZ_UnCompress(U, n2, message_code) # 译码
print(message_)
print(message_ == message)
# True
算术编码
基础知识
算术编码的主要思想是计算输入信源符号序列所对应的区间,然后在区间中任取一点,以其二进制表示适当截断作为序列的编码结果。
设信源符号集
A
=
{
a
1
,
a
2
,
.
.
.
,
a
K
}
A=\{a_1,a_2,...,a_K\}
A={a1,a2,...,aK},其相应的概率分布为
P
(
a
k
)
P(a_k)
P(ak);定义信源符号的分布函数为
F
(
a
k
)
=
∑
i
=
1
k
−
1
P
(
a
i
)
F(a_k)=\sum_{i=1}^{k-1}P(a_i)
F(ak)=i=1∑k−1P(ai)其中
F
(
a
1
)
=
0
F(a_1)=0
F(a1)=0;将
F
(
u
)
F(u)
F(u)写成二进制小数,取小数后
n
n
n位,如有尾数就进位到第
n
n
n位,其中
n
=
⌈
log
1
P
(
u
)
⌉
n=\lceil \log \frac{1}{P(u)}\rceil
n=⌈logP(u)1⌉;得到的数就作为编码结果;(看例子更为直观一些)
例子
设信源 U = { a 1 , a 2 , a 3 , a 4 } U=\{a_1,a_2,a_3,a_4\} U={a1,a2,a3,a4},其概率分布 P ( a 1 ) = 0.5 , P ( a 2 ) = 0.25 , P ( a 3 ) = 0.125 , P ( a 4 ) = 0.125 P(a_1)=0.5,P(a_2)=0.25,P(a_3)=0.125,P(a_4)=0.125 P(a1)=0.5,P(a2)=0.25,P(a3)=0.125,P(a4)=0.125对信源序列 u = a 2 a 1 a 1 a 3 a 4 a 1 a 2 a 1 u=a_2a_1a_1a_3a_4a_1a_2a_1 u=a2a1a1a3a4a1a2a1 做算术编码。
按照编码规则: P ( u ) = ( 0.5 ) 4 ( 0.25 ) 2 ( 0.125 ) 2 = 2 − 14 P(u)=(0.5)^4(0.25)^2(0.125)^2=2^{-14} P(u)=(0.5)4(0.25)2(0.125)2=2−14;码长 n = ⌈ log 1 P ( u ) ⌉ = 14 n=\lceil \log \frac{1}{P(u)}\rceil =14 n=⌈logP(u)1⌉=14;
首先初始概率区间为
a
1
:
[
0
,
0.5
]
,
a
2
:
[
0.5
,
0.75
]
,
a
3
:
[
0.75
,
0.875
]
,
a
4
:
[
0.875
:
1
]
a_1:[0,0.5],a_2:[0.5,0.75],a_3:[0.75,0.875],a_4:[0.875:1]
a1:[0,0.5],a2:[0.5,0.75],a3:[0.75,0.875],a4:[0.875:1]序列第一个字符为
a
2
a_2
a2,那么选定第二个区间为目标区间,再按概率分布划分该区间
a
1
:
[
0.5
,
0.625
]
,
a
2
:
[
0.625
,
0.6875
]
,
a
3
:
[
0.6875
,
0.71875
]
,
a
4
:
[
0.71875
,
0.75
]
a_1:[0.5,0.625],a_2:[0.625,0.6875],a_3:[0.6875,0.71875],a_4:[0.71875,0.75]
a1:[0.5,0.625],a2:[0.625,0.6875],a3:[0.6875,0.71875],a4:[0.71875,0.75]序列第二个字符为
a
1
a_1
a1,那么选定第一个区间为目标区间,再按概率分布划分该区间
a
1
:
[
0.5
,
0.5625
]
,
a
2
:
[
0.5625
,
0.59375
]
,
a
3
:
[
0.59375
,
0.609375
]
,
a
4
:
[
0.609375
,
0.625
]
a_1:[0.5,0.5625],a_2:[0.5625,0.59375],a_3:[0.59375,0.609375],a_4:[0.609375,0.625]
a1:[0.5,0.5625],a2:[0.5625,0.59375],a3:[0.59375,0.609375],a4:[0.609375,0.625]以此类推不再列举,当到序列最后一个字符
a
1
a_1
a1时,确定最终序列
u
u
u所处的区间为:
[
F
(
u
)
,
F
(
u
)
+
P
(
u
)
]
=
[
0.553955078125
,
0.553985595703125
]
[F(u),F(u)+P(u)]=[0.553955078125,0.553985595703125]
[F(u),F(u)+P(u)]=[0.553955078125,0.553985595703125]此时可取区间任一小数,比如取左端点;转换为二进制再取前
n
=
14
n=14
n=14位:10001101110100,没有尾数无需进位;所以最后的编码结果为 10001101110100。
译码过程就是一系列的比较过程,将二进制编码结果转换为小数,比较其位于哪个区间就译为哪个字符;再按同样的方式进行划分,直到译码结束。
实现
编码:先确定码长,再按上述方式不断划分区间;最后取左端点小数的二进制作为编码,如果有尾数则进位;难点在于实现高精度计算,这里在Python使用的是decimal高精度库,当编码规模很大时,计算过高精度会比较慢(实现的问题)
译码:译码需要找到小数所处的区间,可以将每个区间的端点存在一个有序列表,然后采用二分查找加速这个查找过程,找到所处区间就可以译为相应字符,然后同样划分区间;直到译码结束。
from decimal import Decimal, getcontext # 高精度库
from numpy import prod, ceil
# getcontext().prec = 10000 # 设置精度,根据编码规模来设置
def Arithmetic_Compress(message):
U = sorted(list(set(message))) # 字符全集U,按字典序排列
P = {m: Decimal(message.count(m) / len(message)) for m in U} # 字符概率分布,高精度表示
Ps = Decimal(prod([Decimal(P[m] ** message.count(m)) for m in U])) # P(u),高精度表示
n = ceil(-Decimal.log10(Ps) / Decimal.log10(Decimal(2))) # 码长n,log10换底公式,高精度表示
pre = 0 # 初始lower
lower_upper = {} # 初始区间
for m in P:
lower_upper[m] = (pre, pre + P[m]) # 字符i概率所在区间(lower,upper)
pre = pre + P[m] # 上一个字符的upper
for m in message: # 每读取一个字符更新区间
lower, upper = lower_upper[m]
pre = lower
for m_ in P:
new_lower = pre # 新的lower
new_upper = pre + (upper - lower) * P[m_] # 新的upper
pre = new_upper
lower_upper[m_] = (new_lower, new_upper) # 更新区间
flag = lower_upper[message[-1]][0]
message_code = ''
for _ in range(n): # 小数转二进制,每次输出整数位
message_code += str(int(flag * 2))
flag = flag * 2 % 1
if flag != 0: # 有尾数,进位
message_code = int(message_code, 2) + 1
return len(message), U, P, bin(message_code)[2:].zfill(n) # 进位后返回二进制,并给出字符全集和对应的概率分布
return len(message), U, P, message_code # 无尾数,直接返回
def binary_search(lower_upper, flag): # 二分查找概率所在的区间,确定译码字符;
i = 0
j = len(lower_upper) - 1
while i <= j:
mid = (i + j) // 2
if lower_upper[mid] > flag:
j = mid - 1
elif lower_upper[mid] < flag:
i = mid + 1
else:
return mid
return j
def Arithmetic_UnCompress(le, message_code, U, P):
flag = Decimal(0)
exp = Decimal(0.5)
for c in message_code:
if c == '1':
flag = Decimal(Decimal(flag) + Decimal(exp))
exp = Decimal(exp * Decimal(0.5))
old_lower = 0
old_upper = 1
message = ''
for _ in range(le):
lower_upper = []
pre = old_lower
for m in P:
lower = pre
upper = pre + (old_upper - old_lower) * P[m]
lower_upper.append(lower)
pre = upper
lower_upper.append(upper)
ind = binary_search(lower_upper, flag)
message += U[ind]
old_lower = lower_upper[ind]
old_upper = lower_upper[ind + 1]
return message
message = 'baacdaba'
le, U, P, message_code = Arithmetic_Compress(message)
print(message_code)
# 10001101110100
message_ = Arithmetic_UnCompress(le, message_code, U, P)
print(message_)
print(message_ == message)
# True
哈夫曼编码
基础知识
哈夫曼编码得到的码是异字头码,其平均长度最短,是一种最佳码;给定信源 { U , P k } \{U,P_k\} {U,Pk},不失一般性可将各消息按概率由小到大进行排列如下: a 1 , a 2 , . . . , a K a_1,a_2,...,a_K a1,a2,...,aK,相应概率 P 1 ≤ P 2 ≤ P K P_1\le P_2\le P_K P1≤P2≤PK;每次合并概率最小的两个节点构成哈夫曼树,再进行编码;过程比较简单也很熟悉(数据结构,离散对数等课程都会学到);就不再举例关键在于其实现过程。
实现
编码:先根据概率分布创建一棵哈夫曼树,每次从集合选取概率最小的两个节点,合并为一个父节点再插入集合中,直到集合只剩下根节点;这里为了方便可以使用堆结构,动态调整集合;然后获取根到每一个叶子节点(字符)的路径,左0右1,将01串路径作为字符的编码结果;最后编码只需将字符编成相应结果即可。
译码:扫描编码结果,从根游走到叶子节点,然后译成其对应的字符,再返回根;直到译码结束;所以译码方需要重构哈夫曼树或者得到相应的对应关系。
import heapq # 使用堆结构
class TreeNode: # 定义哈夫曼树节点
def __init__(self, char, freq):
self.char = char # 节点携带字符
self.freq = freq # 频率
self.lchild = None # 左孩子
self.rchild = None # 右孩子
def __lt__(self, other): # 自定义比较函数,节点之间比较频率
return self.freq < other.freq
def Create_HuffmanTree(message): # 创建哈夫曼树,每次合并概率最小的两个节点
U = sorted(set(message)) # 字符全集
P = [TreeNode(i, message.count(i) / len(message)) for i in U] # 概率分布,创建节点(char:freq)
heapq.heapify(P) # 原地组织成堆
while len(P) > 1: # 直到堆中只剩根节点,构建完成
l = heapq.heappop(P)
r = heapq.heappop(P) # 依次弹出概率最小的两个节点
father_node = TreeNode(None, l.freq + r.freq) # 合并
father_node.lchild = l # 左孩子
father_node.rchild = r # 右孩子
heapq.heappush(P, father_node) # 加入堆
return P[0] # 返回根节点
def Huffman_Codes(T): # 编码,左0右1,确定根节点到叶子节点的一条路径
def dfs(node, code, result): # 根左右,中序遍历
if node: # 不为空
if node.char: # 找到叶子节点
result[node.char] = code # 对应的路径作为编码
dfs(node.lchild, code + '0', result) # 左0
dfs(node.rchild, code + '1', result) # 右1
huffman_codes = {} # 字符与编码对应关系
dfs(T, '', huffman_codes) # dfs
return huffman_codes
def Huffman_Compress(message, huffman_codes): # 哈夫曼编码,根据前面得到的编码规则来直接编码
message_code = ''
for m in message:
message_code += huffman_codes[m] # 对应的编码
return message_code
def Huffman_UnCompress(message_code, T): # 哈夫曼解码
message = ''
i = 0
while i < len(message_code): # 扫一遍
t = T
while t.char == None: # 根据路径来找,0左右1直到叶子节点
if message_code[i] == '0':
t = t.lchild # 向左
else:
t = t.rchild # 向右
i += 1
message += t.char # 找到,译为叶子节点的字符
return message
message = 'abacbdbdcaad'
T = Create_HuffmanTree(message) # 构建哈夫曼树
huffman_codes = Huffman_Codes(T) # 编码规则
message_code = Huffman_Compress(message, huffman_codes) # 编码
print(message_code)
# 111011001001100100111101
message_ = Huffman_UnCompress(message_code, T) # 解码
print(message_)
print(message_ == message)
# True