支持一下掘金的原創功能~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
。