SICP学习笔记(2.3.4)
SICP学习笔记(2.3.4)
周银辉
1,霍夫曼编码
这节的主要内容是如何用霍夫曼编码算法对消息进行编码和解码,关于这个算法的一些背景知识可以看这里:http://en.wikipedia.org/wiki/Huffman_coding
为了避免我在叙述的时候“断章取义”和你“不知所云”,下面先贴出本节的完整代码(在drscheme4.2.2下编译通过)
(define (make-leaf symbol weight)
(list 'leaf symbol weight))
;判断一个对象是否是叶子节点
(define (leaf? object)
(eq? (car object) 'leaf))
;获取叶子节点上的符号
(define (symbol-leaf x) (cadr x))
;获取叶子节点的权重
(define (weight-leaf x) (caddr x))
;由指定的左子树和右子树构造树
(define (make-code-tree left right)
(list left
right
(append (symbols left) (symbols right))
(+ (weight left) (weight right))))
;获取一棵树的左分支
(define (left-branch tree) (car tree))
;获取一棵树的右分支
(define (right-branch tree) (cadr tree))
;获取一棵树的符号集
(define (symbols tree)
(if (leaf? tree)
(list (symbol-leaf tree))
(caddr tree)))
;获取一棵树的权重
(define (weight tree)
(if (leaf? tree)
(weight-leaf tree)
(cadddr tree)))
;解码,其中bits是编码串,root是编码树的根,tree是当前节点(子树)
(define (decode bits root tree)
(if (null? bits)
'()
(let ((next-branch (choose-branch (car bits) tree)))
(if (leaf? next-branch)
(cons (symbol-leaf next-branch)
(decode (cdr bits) root root))
(decode (cdr bits) root next-branch)))))
;向左走,向右走
(define (choose-branch bit branch)
(cond ((= bit 0) (left-branch branch))
((= bit 1) (right-branch branch))
(else (display "error"))))
;编码消息串,其中message是带编码的消息,root为编码树的根
(define (encode message root)
(if (null? message)
'()
(append (encode-symbol (car message) root)
(encode (cdr message) root))))
;编码单个字符,其中symbol为待编码字符,,tree是当前节点(子树)
(define (encode-symbol symbol tree)
(if (leaf? tree)
;如果树是叶子节点,则返回'()
'()
(let ((left-symbols (symbols (left-branch tree)))
(right-symbols (symbols (right-branch tree))))
(cond
;如果符号在左子树的符号集合中,那么添加编码0,然后用左子树继续编码
((element-of-set? symbol left-symbols)
(cons '0 (encode-symbol symbol (left-branch tree))))
;如果符号在右子树的符号集合中,那么添加编码1,然后用右子树继续编码
((element-of-set? symbol right-symbols)
(cons '1 (encode-symbol symbol (right-branch tree))))
(else
(display "symbol is not in the tree"))))))
(define (element-of-set? x set)
(cond ((null? set) #f)
((eq? x (car set)) #t)
(else
(element-of-set? x (cdr set)))))
;将树插入到一个树的集合中(森林),按照权重升序排列
(define (adjion-set x set)
(cond ((null? set) (list x))
((< (weight x) (weight (car set))) (cons x set))
(else (cons (car set) (adjion-set x (cdr set))))))
;由序对的集合,构造一个有叶子节点构成的集合(最简单的森林)
(define (make-leaf-set pairs)
(if (null? pairs)
'()
(let ((pair (car pairs)))
(adjion-set (make-leaf (car pair) (cadr pair))
(make-leaf-set (cdr pairs))))))
;由序对构造霍夫曼树
(define (generate-huffman-tree pairs)
(successive-merge (make-leaf-set pairs)))
;由叶子节点的集合构造霍夫曼树
(define (successive-merge leaves)
(cond ((= 0 (length leaves)) (display "error at successive-merge"))
((= 1 (length leaves)) (car leaves))
(else
(let
;first为第一个元素,也就是权值最小的树
((first (car leaves))
;second为第二个元素,也就是权值第二小的树
(second (cadr leaves))
;remain为除去第一个与第二个元素后,剩下的元素构成的集合
(remain (cddr leaves)))
(successive-merge
;由权值最小的两个元素构成一棵新的树并添加到剩余集合中
(adjion-set (make-code-tree first second) remain))))))
;-------------------test-----------------------------
(define myTree
(make-code-tree (make-leaf 'A 4)
(make-code-tree
(make-leaf 'B 2)
(make-code-tree
(make-leaf 'D 1)
(make-leaf 'C 1)))))
(define myMessage '(0 1 1 0 0 1 0 1 0 1 1 1 0))
(decode myMessage myTree myTree)
(define myPairs '((A 4) (B 2) (C 1) (D 1)))
(make-leaf-set myPairs)
(define myHuffmanTree (generate-huffman-tree myPairs))
(decode myMessage myHuffmanTree myHuffmanTree)
(encode '(a d a b b c a) myHuffmanTree)
(decode (encode '(a d a b b c a) myHuffmanTree) myHuffmanTree myHuffmanTree)
(display "--------------\n")
(define rockPairs '((A 2) (NA 16) (BOOM 1) (SHA 3) (GET 2) (YIP 9) (JOB 2) (WAH 1)))
(define rockHuffmanTree (generate-huffman-tree rockPairs));
(define rockMessage
'(Get a job
Sha na na na na na na na na
Get a job
Sha na na na na na na na na
Wah yip yip yip yip yip yip yip yip yip
Sha boom))
(encode rockMessage rockHuffmanTree)
(decode (encode rockMessage rockHuffmanTree) rockHuffmanTree rockHuffmanTree)
然后慢慢解释:
一般情况下,我们喜欢将“叶子节点”和“树”统一用tree来表示,因为叶子可以看着左右子树都为空的树,但这里将“叶子”提取出来了,并为它们专门做了构造函数和选择函数。先看关于叶子的几个函数:
(define (make-leaf symbol weight)
(list 'leaf symbol weight))
;判断一个对象是否是叶子节点
(define (leaf? object)
(eq? (car object) 'leaf))
;获取叶子节点上的符号
(define (symbol-leaf x) (cadr x))
;获取叶子节点的权重
(define (weight-leaf x) (caddr x))
非常简单,由于并没有像面向对象编程语言那样用“类”或“结构”来表达“叶子节点”的概念,所以这里用了一个简单的list来表示,但为了区分其与普通列表,所以用'leaf这样引用来做了个标记,其仅仅是一个标记,你可以换成其他任何你喜欢的。
有了leaf这个概念,我们就可以用它来构造“树”了:
(define (make-code-tree left right)
(list left
right
(append (symbols left) (symbols right))
(+ (weight left) (weight right))))
;获取一棵树的左分支
(define (left-branch tree) (car tree))
;获取一棵树的右分支
(define (right-branch tree) (cadr tree))
;获取一棵树的符号集
(define (symbols tree)
(if (leaf? tree)
(list (symbol-leaf tree))
(caddr tree)))
;获取一棵树的权重
(define (weight tree)
(if (leaf? tree)
(weight-leaf tree)
(cadddr tree)))
这其中最重要的是,树的符号集合(symbols),它在待会解码的时候非常有用,否则你在解码时无法确定是应该选择树的左分支还是右分支(当然,你可以采取与SICP中完全不同的算法来查找争取的节点:遍历树节点。但算法复杂度会增加,可能不小心又是一个树形递归了)
至此,你已经可以表示出一颗树了,只不过是手动的,正如下面的代码一样(练习2.67):
(make-code-tree (make-leaf 'A 4)
(make-code-tree
(make-leaf 'B 2)
(make-code-tree
(make-leaf 'D 1)
(make-leaf 'C 1)))))
将它翻译成列表的形式,结果如下:
((leaf a 4) ((leaf b 2) ((leaf d 1) (leaf c 1) (d c) 2) (b d c) 4) (a b d c) 8)
似乎有些晦涩(实际上这只是一个中间值,在完整的编码和解码过程中,我们是不需要关心这些晦涩的列表的)但无论怎么样,我们可以编写针对它的编码和解码函数了。
先看编码函数:
(define (encode message root)
(if (null? message)
'()
(append (encode-symbol (car message) root)
(encode (cdr message) root))))
;编码单个字符,其中symbol为待编码字符,,tree是当前节点(子树)
(define (encode-symbol symbol tree)
(if (leaf? tree)
;如果树是叶子节点,则返回'()
'()
(let ((left-symbols (symbols (left-branch tree)))
(right-symbols (symbols (right-branch tree))))
(cond
;如果符号在左子树的符号集合中,那么添加编码0,然后用左子树继续编码
((element-of-set? symbol left-symbols)
(cons '0 (encode-symbol symbol (left-branch tree))))
;如果符号在右子树的符号集合中,那么添加编码1,然后用右子树继续编码
((element-of-set? symbol right-symbols)
(cons '1 (encode-symbol symbol (right-branch tree))))
(else
(display "symbol is not in the tree"))))))
要对一个符号串进行编码,那么先对这个串的第一个符号进行编码,然后追加上对其子串的编码(注意到,这句话是递归的,也正如我们的encode函数所描述的那样),OK,现在的关键点就是如何对单个符号进行编码?其实很简单,你甚至可以简单地理解为:从根节点出发,找到该符号所在的叶子节点,并记录下正确的寻找路径。这个搜索过程也就是编码过程。
我们从根作为起始点,如果左子树的符号集合中包含了我们要寻找的符号,那么说明其在左子树或左子树的某个子树中,然后再以左子树的根作为起始点重新开始搜索过程,此时,我们永远不用搜索右子树了,这是不是很像“折半查找”。其时间复杂度为log(2)n。
然后是解码过程,解码就相当简单了,“编码”就像“地址”一样,拿着“地球中国北京长安街110号”这样的编码要找到这个地点还不容易,并且huffman编码还要简单一点,因为它告诉你“看到0向左走,看到1向右走”:
(define (decode bits root tree)
(if (null? bits)
'()
(let ((next-branch (choose-branch (car bits) tree)))
(if (leaf? next-branch)
(cons (symbol-leaf next-branch)
(decode (cdr bits) root root))
(decode (cdr bits) root next-branch)))))
;向左走,向右走
(define (choose-branch bit branch)
(cond ((= bit 0) (left-branch branch))
((= bit 1) (right-branch branch))
(else (display "error"))))
能顺利地编码和解码了,似乎工作做完了。不对,因为我们刚才建立的那颗霍夫曼编码树是手动编写的,如果符号较多的话,手动编写编码树就不显示了,所以还需要一点自动化工作,我们告诉计算机所有的符号以及符号所对应的权值,让计算机帮我们构造霍夫曼编码树。
这需要分两步走,首先由符号和权值构成序对,序对构成序对集合,让每个序对构成一颗最简单的树(叶子节点),这些树再构成一个集合(森林);然后由这些松散的树的集合构成一颗大树(一个完整的霍夫曼编码树)。
构造森林的时候有一点点小技巧,由于霍夫曼编码算法中,要求取出权值最小的两个节点进行合并以构成新的节点。要取得最小的“两个”,最简单的方法就是对列表进行排序,然后取第一个和第二个(如果升序的话),可惜,我们还没有学习Scheme的排序排序算法呢,所以这里玩的一点小聪明是:列表再构造是就让它有序地构造,也就是下面的代码所干的事情:
(define (adjion-set x set)
(cond ((null? set) (list x))
((< (weight x) (weight (car set))) (cons x set))
(else (cons (car set) (adjion-set x (cdr set))))))
再看构造霍夫曼树的代码:
(define (make-leaf-set pairs)
(if (null? pairs)
'()
(let ((pair (car pairs)))
(adjion-set (make-leaf (car pair) (cadr pair))
(make-leaf-set (cdr pairs))))))
;由序对构造霍夫曼树
(define (generate-huffman-tree pairs)
(successive-merge (make-leaf-set pairs)))
;由叶子节点的集合构造霍夫曼树
(define (successive-merge leaves)
(cond ((= 0 (length leaves)) (display "error at successive-merge"))
((= 1 (length leaves)) (car leaves))
(else
(let
;first为第一个元素,也就是权值最小的树
((first (car leaves))
;second为第二个元素,也就是权值第二小的树
(second (cadr leaves))
;remain为除去第一个与第二个元素后,剩下的元素构成的集合
(remain (cddr leaves)))
(successive-merge
;由权值最小的两个元素构成一棵新的树并添加到剩余集合中
(adjion-set (make-code-tree first second) remain))))))
2,练习2.67
代码上面已经包含了,解码结果为: (a d a b b c a)
3,练习2.68
(define (encode message root)
(if (null? message)
'()
(append (encode-symbol (car message) root)
(encode (cdr message) root))))
;编码单个字符,其中symbol为待编码字符,,tree是当前节点(子树)
(define (encode-symbol symbol tree)
(if (leaf? tree)
;如果树是叶子节点,则返回'()
'()
(let ((left-symbols (symbols (left-branch tree)))
(right-symbols (symbols (right-branch tree))))
(cond
;如果符号在左子树的符号集合中,那么添加编码0,然后用左子树继续编码
((element-of-set? symbol left-symbols)
(cons '0 (encode-symbol symbol (left-branch tree))))
;如果符号在右子树的符号集合中,那么添加编码1,然后用右子树继续编码
((element-of-set? symbol right-symbols)
(cons '1 (encode-symbol symbol (right-branch tree))))
(else
(display "symbol is not in the tree"))))))
4,练习2.69
(define (successive-merge leaves)
(cond ((= 0 (length leaves)) (display "error at successive-merge"))
((= 1 (length leaves)) (car leaves))
(else
(let
;first为第一个元素,也就是权值最小的树
((first (car leaves))
;second为第二个元素,也就是权值第二小的树
(second (cadr leaves))
;remain为除去第一个与第二个元素后,剩下的元素构成的集合
(remain (cddr leaves)))
(successive-merge
;由权值最小的两个元素构成一棵新的树并添加到剩余集合中
(adjion-set (make-code-tree first second) remain))))))
5,练习2.70
编码结果为:
(1 1 1 1 1 1 1 0 0 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 0 1 1 0 1 1)
一共84位,如果使用定长编码的话则需要288位,看来是节省了不少。
6,练习2.71
其始终朝一边延伸的,最小长度为1,最大为N-1
7,练习2.72
由于我上面给出的element-of-set?是线性的,设其计算步骤数位n,对某一个符号进行编码时,如果该符号在树的第m层,那么其编码步骤为m*n,如果待编码的符号总数为s,那么总的编码步骤为s*m*n,所以时间复杂度为n^3,对于最频繁的符号,其在树的第一层,所以计算步骤为1*n,时间复杂度为线性,对于最不频繁的符合,其在树的最后一层,那么其编码步骤为m*n,时间复杂度为n^2,所以,如果能利用2.3.3中的知识,降低element-of-set?的复杂度,将节约不少时间。
注:这是一篇读书笔记,所以其中的内容仅 属个人理解而不代表SICP的观点,并随着理解的深入其中 的内容可能会被修改