# 2.3.4示例：霍夫曼编码树

> 译者： [https://sicp.comp.nus.edu.sg/chapters/38](https://sicp.comp.nus.edu.sg/chapters/38)

本节提供了使用列表结构和数据抽象来操作集和树的实践。 本申请涉及用于将数据表示为一和零（位）序列的方法。 例如，用于表示计算机中文本的ASCII标准代码将每个字符编码为7位序列。 使用七个位可以区分![2%5E7](img/19853e38f9298014e0dac9bcf6fa06e2.jpg)或128个可能的不同字符。 通常，如果要区分![n](img/493731e423d5db62086d0b8705dda0c8.jpg)个不同的符号，则每个符号需要使用![%5Clog_2%20n](img/13e13f9bd3cff758cefb9f4093f4db3c.jpg)个位。 如果我们所有的消息都由八个符号A，B，C，D，E，F，G和H组成，则我们可以选择每个字符三个位的代码，例如![](img/da6543367bce9ddee8e6c10bc2968efe.jpg)使用此代码，消息

> BACADAEAFABBAAAGAH

is encoded as the string of 54 bits

> 001000010000011000100000101000001001000000000110000111

Codes such as ASCII and the A-through-H code above are known as _fixed-length_ codes, because they represent each symbol in the message with the same number of bits. It is sometimes advantageous to use _variable-length_ codes, in which different symbols may be represented by different numbers of bits. For example, Morse code does not use the same number of dots and dashes for each letter of the alphabet. In particular, E, the most frequent letter, is represented by a single dot. In general, if our messages are such that some symbols appear very frequently and some very rarely, we can encode data more efficiently (i.e., using fewer bits per message) if we assign shorter codes to the frequent symbols. Consider the following alternative code for the letters A through H: ![](img/32b2d41cde005e1a6367e3143928cf8f.jpg) With this code, the same message as above is encoded as the string

> 100010100101101100011010100100000111001111

This string contains 42 bits, so it saves more than 20% in space in comparison with the fixed-length code shown above.

使用可变长度代码的困难之一是知道何时到达符号的末尾以读取零和一的序列。 摩尔斯电码通过在每个字母的点和破折号序列之后使用特殊的_分隔符_（在这种情况下为暂停）来解决此问题。 另一种解决方案是以这种方式设计代码，即没有任何符号的完整代码是另一个符号的代码的开头（或_前缀_）。 这样的代码称为_前缀代码_。 在上面的示例中，A用0编码，B用100编码，因此没有其他符号可以具有以0或100开头的代码。

通常，如果我们使用可变长度前缀码来利用要编码的消息中符号的相对频率，则可以节省大量资金。 为此，一种特殊的方案称为Huffman编码方法，以其发现者David Huffman为例。 霍夫曼代码可以表示为二叉树，其叶子是已编码的符号。 在树的每个非叶子节点处，都有一个集合，其中包含该节点下方叶子中的所有符号。 另外，在叶子上的每个符号都分配有权重（即它的相对频率），每个非叶子节点都包含一个权重，该权重是位于它下面的叶子的所有权重的总和。 在编码或解码过程中不使用权重。 我们将在下面看到它们如何用于帮助构建树。

<figure>![](img/b8f192a71ba61475def7ab6b237fe08d.jpg)**[图2.18](38#fig_2.18)** Huffman编码树。</figure>

图 <ref name="fig:huffman">[2.18](38#fig_2.18)</ref> 显示了上面给出的A-H代码的霍夫曼树。 叶子上的权重表示该树是为消息设计的，其中A出现的相对频率为8，B出现的相对频率为3，其他字母的出现频率分别为1。

给定一个霍夫曼树，我们可以从根开始向下移动直到到达保存该符号的叶子，从而找到任何符号的编码。 每次我们向左分支下移，我们都会向代码添加0，每次我们向右分支下移，我们都会添加1。（我们通过测试确定哪个分支是哪个分支节点是叶子节点） 例如，从图 [2.18](38#fig_2.18) 的树的根开始，我们通过右分支到达D的叶子，然后 左分支，然后是右分支，然后是右分支； 因此，D的代码为1011。

为了使用霍夫曼树对位序列进行解码，我们从根开始，并使用连续的零和位序列中的一个来确定是向下移动还是向左分支移动。 每次来到叶子时，我们都会在消息中生成一个新符号，这时我们将从树的根部开始查找下一个符号。 例如，假设给了上面的树和序列10001010。从根开始，我们向下移动右分支（因为字符串的第一位是1），然后向下移动左分支（因为第二位是 0），然后沿左分支向下移动（因为第三位也是0）。 这将我们带到B的叶子，因此已解码消息的第一个符号是B.现在我们再次从根开始，然后左移，因为字符串的下一位是0.这将我们带到叶子 for A.然后我们从字符串1010的其余部分重新开始，所以我们向右，向左，向右，向左移动并到达C.因此，整个消息就是BAC。

## 生成霍夫曼树

给定符号的&lt;quote&gt;字母&lt;/quote&gt;及其相对频率，我们该如何构建&lt;quote&gt;最佳&lt;/quote&gt;码？ （换句话说，哪棵树将用最少的比特对消息进行编码？）霍夫曼给出了一种算法来执行此操作，并显示出结果代码的确是消息的最佳可变长度代码，其中符号的相对频率与频率相对应。 代码的构造方式。 我们不会在这里证明霍夫曼码的这种最优性，但是我们将展示霍夫曼树是如何构造的。 [[1]](38#footnote-1)

生成霍夫曼树的算法非常简单。 想法是安排树，以使频率最低的符号看起来离根最远。 从包含符号及其频率的叶节点开始，该叶节点由要构造代码的初始数据确定。 现在找到权重最低的两片叶子，并将它们合并以产生一个节点，该节点的左，右两个分支都为这两个节点。 新节点的权重是两个权重之和。 从原始集中删除两片叶子，然后用这个新节点替换它们。 现在继续此过程。 在每个步骤中，合并两个权重最小的节点，将其从集合中删除，然后将其替换为以这两个为左，右分支的节点。 当仅剩一个节点（整个树的根）时，该过程停止。 这是图的霍夫曼树的生成方式 [2.18](38#fig_2.18)![](img/182cf4571ca7c9e4cc74180ba697b142.jpg)&lt;/split&gt; 该算法并不总是指定唯一树，因为可能没有唯一的最小树 每一步的权重节点。 而且，两个节点的合并顺序的选择是任意的（即，将是右分支而将是左分支）。

## 代表霍夫曼树

在下面的练习中，我们将使用一个系统使用霍夫曼树对消息进行编码和解码，并根据上述算法生成霍夫曼树。 我们将首先讨论树木的表示方式。

树的叶子由包含符号`leaf`，叶子处的符号和权重的列表表示：

```js
function make_leaf(symbol, weight) {
   return list("leaf", symbol, weight);
}
function is_leaf(object) {
   return head(object) === "leaf";
}
function symbol_leaf(x) {
  return head(tail(x));
}
function weight_leaf(x) {
   return head(tail(tail(x)));
}
```

通用树将是左分支，右分支，一组符号和权重的列表。 符号集将只是符号列表，而不是一些更复杂的集表示形式。 当我们通过合并两个节点来制作树时，我们获得树的权重作为节点权重的总和，并获得符号集作为节点符号集的并集。 由于符号集被表示为列表，因此我们可以使用在 [2.2.1](30) 部分中定义的`append`函数形成联合。

```js
function make_code_tree(left,right) {
    return list(left,
                right,
                append(symbols(left), symbols(right)),
                weight(left) + weight(right));
}
```

If we make a tree in this way, we have the following selectors:

```js
function left_branch(tree) {
    return head(tree);
}
function right_branch(tree) {
    return head(tail(tree));
}
function symbols(tree) {
    return is_leaf(tree)
           ? list(symbol_leaf(tree))
           : head(tail(tail(tree)));
}
function weight(tree) {
    return is_leaf(tree)
           ? weight_leaf(tree)
           : head(tail(tail(tail(tree))));
}
```

函数`symbols`和`weight`的功能必须稍有不同，具体取决于它们是用叶子还是一般的树来调用。 这些是_泛型函数_（可以处理多种数据的函数）的简单示例，我们将在 [2.4](39)[2.5](43) 。

## 解码功能

以下功能实现了解码算法。 它以零和一的列表以及霍夫曼树作为参数。

```js
function decode(bits, tree) {
    function decode_1(bits, current_branch) {
        if (is_null(bits)) {
            return null;
        } else {
            const next_branch = choose_branch(head(bits),
                                    current_branch);
            return is_leaf(next_branch)
                   ? pair(symbol_leaf(next_branch),
                          decode_1(tail(bits), tree))
                   : decode_1(tail(bits), next_branch);
        }
    }
    return decode_1(bits, tree);
}
function choose_branch(bit, branch) {
    return bit === 0
           ? left_branch(branch)
           : bit === 1
             ? right_branch(branch)
             : Error("bad bit -- choose_branch",bit);
}
```

函数`decode_1`具有两个参数：剩余位列表和树中的当前位置。 它继续将&lt;quote&gt;向下移动&lt;/quote&gt;树，根据列表中的下一个位是0还是1选择左分支或右分支。 （这是通过`choose_branch`函数完成的。）到达叶子时，它将通过`pair`将其在该叶子上的符号作为消息中的下一个符号返回，将其解码为对其余消息进行解码的结果，开始 在树的根部。 注意`choose_branch`的最后一个子句中的错误检查，该子句抱怨函数是否在输入数据中找到零或一以外的东西。

## 加权元素集

在树的表示中，每个非叶节点都包含一组符号，这些符号已被表示为一个简单列表。 但是，上面讨论的生成树的算法要求我们还使用叶子和树的集合，将两个最小的项相继合并。 由于将需要我们反复查找集合中的最小项，因此对于此类集合使用有序表示很方便。

我们将一组叶子和树木表示为元素列表，以重量顺序递增。 以下用于构建集合的`adjoin_set`函数类似于练习 [2.61](37#ex_2.61) 中描述的函数； 但是，将根据项目的权重进行比较，并且从未添加到集合中的元素。

```js
function adjoin_set(x, set) {
    return is_null(set)
           ? list(x)
           : weight(x) < weight(head(set))
             ? pair(x, set)
             : pair(head(set), adjoin_set(x, tail(set)));
}
```

以下函数获取符号频率对的列表，例如

```js
list(list("A", 4), list("B", 2), list("C", 1), list("D", 1))
```

and constructs an initial ordered set of leaves, ready to be merged according to the Huffman algorithm:

```js
function make_leaf_set(pairs) {
   if (is_null(pairs)) {
      return null;
   }
   else {
      const first_pair = head(pairs);
      return adjoin_set(
                 make_leaf(head(first_pair),        // symb
                           head(tail(first_pair))), // freq
                 make_leaf_set(tail(pairs)));
   }
}
```

<exercise>Define an encoding tree and a sample message:

```js
const sample_tree =
    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))));
const sample_message =						 
    list(0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0);
```

Use the `decode` function to decode the message, and give the result.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_38_1_div">Solution</button> <solution>```js
decode(sample_message, sample_tree);
// should be: ["A", ["D", ["A", ["B", ["B", ["C", ["A", null]]]]]]]
```</solution></exercise> <exercise>The `encode` function takes as arguments a message and a tree and produces the list of bits that gives the encoded message.

```js
function encode(message, tree) {
    return is_null(message)
           ? null
           : append(encode_symbol(head(message), tree),
                    encode(tail(message), tree));
}
```

<split>Write the function `encode_symbol` that returns the list of bits that encodes a given symbol according to a given tree.</split> You should design `encode_symbol` so that it signals an error if the symbol is not in the tree at all. Test your function by encoding the result you obtained in exercise <ref name="ex:sample-tree">[2.67](38#ex_2.67)</ref> with the sample tree and seeing whether it is the same as the original sample message.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_38_2_div">Solution</button> <solution>```js
function encode_symbol(symbol, tree) {
    function contains_symbol(symbol, current_tree) {
        return member(symbol, symbols(current_tree)) !== null;
    }
    if (is_leaf(tree)) {
        return null;
    } else {
        const left_tree = left_branch(tree);
	const right_tree = right_branch(tree);
	return contains_symbol(symbol, left_tree)
               ? pair(0, encode_symbol(symbol, left_tree))
                 : contains_symbol(symbol, right_tree)
                   ? pair(1, encode_symbol(symbol, right_tree))
                   : error("symbol not found");
    }
}
```</solution></exercise> <exercise>The following function takes as its argument a list of symbol-frequency pairs (where no symbol appears in more than one pair) and generates a Huffman encoding tree according to the Huffman algorithm.

```js
function generate_huffman_tree(pairs) {
   return successive_merge(make_leaf_set(pairs));
}
```

<split>The function `make_leaf_set` that transforms the list of pairs into an ordered set of leaves is given above. Write the function `successive_merge` using `make_code_tree` to successively merge the smallest-weight elements of the set until there is only one element left, which is the desired Huffman tree.</split> (This function is slightly tricky, but not really complicated. If you find yourself designing a complex function, then you are almost certainly doing something wrong. You can take significant advantage of the fact that we are using an ordered set representation.)<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_38_3_div">Solution</button> <solution>```js
function successive_merge(leaves) {
    return length(leaves) === 1
           ? head(leaves)
           : successive_merge(
                 adjoin_set(
                     make_code_tree(head(leaves),
                                    head(tail(leaves))),
                     tail(tail(leaves))));
}
```</solution></exercise> <exercise>The following eight-symbol alphabet with associated relative frequencies was designed to efficiently encode the lyrics of 1950s rock songs. (Note that the <quote>symbols</quote> of an <quote>alphabet</quote> need not be individual letters.) ![](img/6ea62b43b0ebc8d77bf5555b54a5a836.jpg) Use `generate_huffman_tree` (exercise <ref name="ex:generate-huffman-tree">[2.69](38#ex_2.69)</ref>) to generate a corresponding Huffman tree, and use `encode` (exercise <ref name="ex:encode-symbol">[2.68](38#ex_2.68)</ref>) to encode the following message:

得到一份工作
做一份工作
得到一份工作
做一份工作
Wah yip yip yip yip yip yip yip yip
Sha 繁荣

How many bits are required for the encoding? What is the smallest number of bits that would be needed to encode this song if we used a fixed-length code for the eight-symbol alphabet?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_38_4_div">Solution</button> <solution>```js
const lyrics_frequencies = 	  
    list(list("A", 2),
         list("NA", 16),
         list("BOOM", 1),
         list("SHA", 3),
         list("GET", 2),
         list("YIP", 9),
         list("JOB", 2),
         list("WAH", 2));
const lyrics_tree = generate_huffman_tree(lyrics_frequencies);
const lyrics = list(
    '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'
   );

length(encode(lyrics, lyrics_tree));
// 84
```

We have an alphabet of ![n%20%3D%208](img/c8b06f11ec052bf3f223cc3dea61886c.jpg) symbols, and a message of ![m%20%3D%2036](img/ed7973873fccf5d0e2d18f1ea9a79d17.jpg) symbols. Then the minimum number of bits to encode a specific symbol using a fixed-length code is ![%5Clceil%20%5Clog_2%7Bn%7D%20%5Crceil%20%3D%203](img/88a127907c3f761f9d14fe926da0eaa7.jpg). Thus the minimum number of bits to encode all the lyrics is ![m%20%5Clceil%20%5Clog_2%7Bn%7D%20%5Crceil%20%3D%2036%20%5Ctimes%203%20%3D%20108](img/99826aa5189974869074849cc238060c.jpg).</solution></exercise><exercise>Suppose we have a Huffman tree for an alphabet of ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) symbols, and that the relative frequencies of the symbols are 1, 2, 4, …, ![2%5E%7Bn-1%7D](img/5c028f7578ec77c729b895309f14ab4c.jpg). Sketch the tree for ![n](img/493731e423d5db62086d0b8705dda0c8.jpg)=5; for ![n](img/493731e423d5db62086d0b8705dda0c8.jpg)=10\. In such a tree (for general ![n](img/493731e423d5db62086d0b8705dda0c8.jpg)) how may bits are required to encode the most frequent symbol? the least frequent symbol?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_38_5_div">Solution</button> <solution>该树将是不平衡的，类似于图 [2.17](37#fig_2.17) 中给出的树。 编码最频繁的符号需要一位，而![n%20-%201](img/6a5f27b69ff34f05786b41f86ccfbbc7.jpg)位则需要编码最不频繁的符号。</solution></exercise> <exercise>Consider the encoding function that you designed in exercise <ref name="ex:encode-symbol">[2.68](38#ex_2.68)</ref>. What is the order of growth in the number of steps needed to encode a symbol? Be sure to include the number of steps needed to search the symbol list at each node encountered. To answer this question in general is difficult. Consider the special case where the relative frequencies of the ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) symbols are as described in exercise <ref name="ex:huffman-count">[2.71](38#ex_2.71)</ref>, and give the order of growth (as a function of ![n](img/493731e423d5db62086d0b8705dda0c8.jpg)) of the number of steps needed to encode the most frequent and least frequent symbols in the alphabet.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_38_6_div">Solution</button><solution>Consider the special case in exercise <ref name="ex:encode-symbol">[2.68](38#ex_2.68)</ref>. At each step down the path of length ![n](img/493731e423d5db62086d0b8705dda0c8.jpg), we need to do a linear search in a list of length ![n%2C%20n-1%2C%20%5Cldots%2C%201](img/5313c4f92d7829b7495732d63958e345.jpg). In the worst case, there are ![O%28n%20%5Ctimes%20n%20%2F%202%29%20%3D%20O%28n%5E2%29](img/c3e21a3592ce7949e33a4f1919411eaa.jpg) number of steps. ![O%28n%5E2%29](img/6ba352bc46c0f8b10f802e59f9bf5450.jpg).</solution></exercise>

* * *

[[1]](38#footnote-link-1) See <citation>Hamming 1980</citation> for a discussion of the mathematical properties of Huffman codes.

