支持一下掘金的原创功能~node
原文地址:哈夫曼编码 —— Lisp 与 Python 实现python
SICP 第二章主讲对数据的抽象,能够简单地理解为 Lisp 语言的数据结构。固然这样理解只可做为一种过渡,由于 Lisp 中并不存在数据与过程的差异,也就彻底不须要一个额外的概念来描述这种对数据抽象的过程。2.3.4 以哈弗曼编码为例展现了如何在 Lisp 中实现哈夫曼二叉树数据结构的表示与操做,本文在完成该小节习题(完整的哈夫曼编码树生成、解码与编码)的基础上,将 Lisp(这里用的是 DrRacket 的 #lang sicp
模式,即近似于 Scheme
)与 Python 版本的程序进行对比。算法
哈夫曼编码基于加权二叉树,在 Python 中表示哈夫曼树:编程
class Node(object):
def __init__(self, symbol='', weight=0):
self.left = None
self.right = None
self.symbol = symbol # 符号
self.weight = weight # 权重复制代码
Lisp 经过列表(List)表示:缓存
(define (make-leaf symbol weight) (list 'leaf symbol weight))
(define (make-tree left right)
(list left
right
(append (symbols left) (symbols right)) ; 将叶子的字符缓存在根节点
(+ (weight left) (weight right)))
)
;; methods
(define (leaf? object) (eq? (car object) 'leaf))
(define (symbol-leaf leaf) (cadr leaf))
(define (weight-leaf leaf) (caddr leaf))
(define (symbols tree)
(if (leaf? tree)
(list (symbol-leaf tree))
(caddr tree)))
(define (weight tree)
(if (leaf? tree)
(weight-leaf tree)
(cadddr tree)))复制代码
为了生成最优二叉树,哈夫曼编码以字符出现的频率做为权重,每次选择目前权重最小的两个节点做为生成二叉树的左右分支,并将权重之和做为根节点的权重,按照这一贪婪算法,自底向上生成一棵带权路径长度最短的二叉树。数据结构
依据这一算法,能够从“字符-频率”的统计信息中创建一棵哈夫曼树,在 Python 实现中,只须要每次对全部节点从新按照权重排序便可:app
# frequency = {'a': 1, 'b': 3, 'c': 2}
def huffman_tree(frequency):
SIZE = len(frequency)
nodes = [Node(char, frequency.get(char)) for char in frequency.keys()]
for _ in range(SIZE - 1):
nodes.sort(key=lambda n: n.weight) # 对全部节点按照权重从新排序
left = nodes.pop(0)
right = nodes.pop(0)
parent = Node('', left.weight + right.weight)
parent.left = left
parent.right = right
nodes.append(parent)
return nodes.pop()复制代码
Lisp 一般采用递归的过程完成循环操做,一种相似插入排序的有序集合实现以下:函数
(define (adjoin-set x set)
(cond ((null? set) (list x))
((< (weight x) (weight (car set))) (cons x set))
(else (cons (car set) (adjoin-set x (cdr set))))))复制代码
下面这段与 Python 列表推断功能类似的过程,这也许可让你更加感觉到 Python 的简洁与美妙:学习
(define (make-leaf-set pairs)
(if (null? pairs)
'()
(let ((pair (car pairs)))
(adjoin-set (make-leaf (car pair)
(cadr pair))
(make-leaf-set (cdr pairs))))))复制代码
最后,基于“字符-频率”的有序集合生成哈夫曼编码树:ui
(define (generate-huffman-tree pairs)
(define (successive-merge leaf-set)
(cond ((null? leaf-set) '())
((null? (cdr leaf-set)) (car leaf-set))
(else (successive-merge (adjoin-set
(make-code-tree (car leaf-set)
(cadr leaf-set))
(cddr leaf-set))))
))
(successive-merge (make-leaf-set pairs)))复制代码
有了哈夫曼树以后,能够进行编码和解码。编码过程须要找到字符所在的叶子节点,以及从根节点到该叶子节点的路径,每次通过左子树搜索记做"0"
,通过右子树记做"1"
,所以能够利用二叉树的先序遍历,递归地找到叶子节点和路径:
def encode(symbol, tree):
bits = None
def preOrder(tree, path):
if tree.left:
preOrder(tree.left, path + "0")
if tree.right:
preOrder(tree.right, path + "1")
if tree.isLeaf() and tree.symbol == symbol:
nonlocal bits
bits = path
preOrder(tree, "")
return bits复制代码
Lisp 的二叉树中每层根节点缓存了全部子树中的出现的字符,以空间换取时间:
(define (left-branch tree) (car tree))
(define (right-branch tree) (cadr tree))
(define (encode-symbol char tree)
(cond ((null? char) '())
((leaf? tree) '())
((memq char (symbols (left-branch tree)))
(cons 0
(encode-symbol char (left-branch tree))))
((memq char (symbols (right-branch tree)))
(cons 1
(encode-symbol char (right-branch tree))))
(else (display "Error encoding char "))))复制代码
解码过程更直接一些,只要遵循遇到"0"
取左子树,遇到"1"
取右子树的规则便可:
def decode(bits, tree):
result = ""
root = tree
for bit in bits:
if bit == "0":
node = root.left
elif bit == "1":
node = root.right
else:
return "Code error: {}".format(bit)
if node.isLeaf():
result += node.symbol
root = tree
else:
root = node
return result复制代码
(define (choose-branch bit branch)
(cond ((= bit 0) (left-branch branch))
((= bit 1) (right-branch branch))
(else (display "bat bit: CHOOSE-BRANCH" bit))))
(define (decode bits tree)
(define (decode-1 bits current-branch)
(if (null? bits)
'()
(let ((next-branch
(choose-branch (car bits) current-branch)))
(if (leaf? next-branch)
(cons (symbol-leaf next-branch)
(decode-1 (cdr bits) tree))
(decode-1 (cdr bits) next-branch)))))
(decode-1 bits tree))复制代码
须要注意的是上面的算法并不能保证每次生成的哈夫曼树都是惟一的,由于可能出现权值相等以及左右子树分配的问题,可是对于同一棵树,编码和解码的结果是互通的。
虽然未必会用到 Lisp 做为开发语言,但并不妨碍咱们学习、吸取其中优秀的思想。SICP 前两章分别介绍了对过程的抽象和对数据的抽象,其中一个重要的思想就是编码的本质是对计算过程的描述,而这种描述并不拘泥于某种特定的语法或数据结构;对过程的抽象(例如(define (add a b) (+ a b))
)与对数据的抽象(例如(define (make-leaf symbol weight) (list 'leaf symbol weight))
)之间并无本质的差别。
Python 在确保简介、优雅的同时也拥有惊人的灵活性,咱们甚至能够模仿 Scheme
的语法来完成上面的全部程序:
def make-leaf(symbol, weight):
return ['leaf', symbol, weight]
def leaf?(obj):
return obj[0] == 'leaf'复制代码
虽然像上面这样作毫无心义,可是将对过程的描述抽象到函数层面,而后对函数进行操做的思想在 Python 中一样很是重要。
上面的代码大部分是在旅途中的火车或汽车上完成的,少有这样的机会体验一下离线编程的“乐趣”,sort
和 nonlocal
的用法还要多亏写 PyTips 时的总结,所以仍是但愿有时间能够写满 0xFF
。