1.1 定义
二叉树的定义是一种递归定义,它的特点是每个结点至多只存在两棵子树(即二叉树中不存在度大于2的结点),且左右子树有先后顺序之分。一棵二叉树要么是空树,要么是一个根结点加上左、右子树组成的二叉树。其中,左右子树也都是二叉树。
1.2 性质
二叉树的常见性质:
一棵深度为 h h h,且有 2 h − 1 2^h-1 2h−1 个结点的二叉树称为满二叉树。如下图,特点是每一层上的结点都是最大结点数
假设堆满二叉树的结点进行连续编号,则深度为 h h h,结点树为 n n n 的二叉树,若其结点编号与一个深度为 h h h 的满二叉树一一对应
,则称之为 完全二叉树,如下图:
完全二叉树的特点:
i
的左孩子为2i+1
,右孩子为2i+2
i
的,父亲结点为 ⌊ ( i − 1 ) / 2 ⌋ \left \lfloor (i-1)/2 \right \rfloor ⌊(i−1)/2⌋哈夫曼树(Huffman),又称最优树,是一类带权路径长度最短的树
。
3.1 最优二叉树
相关概念
构造算法
假设有4个权值,7,5,2,4
,则下面三棵二叉树来说,树©的带权路径长度最短,为我们需要的哈夫曼树。
则我们有个直观的感受:权值越大的需要离根结点越近,权值越小的可以离根结点远一些,这样,最后总的带权路径长度会更小。 所以,我们有如下最优二叉树的构造算法:
3.2 应用——哈夫曼编码
引出
在信息通信中,我们需要给通信字符编码,且通常有2点原则:
而我们可以用二叉树进行编码,例如对于四个字符A, B, C,D,使用二叉树可以如下编码:
其肯定是前缀编码,要使得最短编码,即对于每种字符的出现次数 w i w_i wi 和其编码长度 l i l_i li 的乘积总和最小,即 ∑ k = 1 n w k l k \sum_{k=1}^n w_k l_k ∑k=1nwklk 最小。这表明,我们需要根据字符的出现频率构造一棵哈夫曼树。
构造
由于哈夫曼树中没有度为1的结点,则根据二叉树的性质,有 n n n 个叶子结点的哈夫曼树的总结点为 n + n − 1 = 2 n − 1 n + n - 1 = 2n-1 n+n−1=2n−1,则可以存储在长度为 2 n − 1 2n-1 2n−1 的一维数组中。
构造完成后,
所以,对于每个结点,我们需要知道其孩子和父亲结点,则我们引入下列数据结构:
结点定义
typedef struct {
int weight;
int left, right, parent;
}Hnode,*Htree;
哈夫曼编码本
typedef string* HuffmanCode;
编码过程
void HuffmanCoding(Htree& HT, HuffmanCode& HC, int* w, int n) {
/* * w为结点权重,n * HT 为哈夫曼树的结点数组表示,2n+1 * HC 为结点的编码映射,n */
if (n <= 1) return;
int m = 2 * n - 1;
HT = new Hnode[m + 1]; // 0号单元未用,表示初始结点的根结点,代表NULL
// 初始化各叶子结点
int i;
for (i = 0; i < n; i++)
HT[i + 1] = { w[i],0,0,0};
// 初始化要构造的结点
for (; i < m; i++)
HT[i + 1] = { 0,0,0,0 };
// 开始构造内部结点
for (i = n + 1; i <= m; i++) {
// 挑选两个权重最小的结点,且它们的父亲结点为0,表示还未加入树
int s1 = -1, s2 = -1;
int min1 = INF, min2 = INF;
for (int j = 1; j < i; j++) {
if (HT[j].parent != 0) continue;
if (HT[j].weight < min1) {
min2 = min1; s2 = s1; // 先更新次小
min1 = HT[j].weight; s1 = j;
}
else if (HT[j].weight < min2) {
min2 = HT[j].weight; s2 = j;
}
}
// 将两个次小的结点加入
HT[s1].parent = HT[s2].parent = i;
HT[i].left = s1; HT[i].right = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
if (DEBUG) printf("孩子结点%d,%d 父亲结点%d\n", HT[s1].weight, HT[s2].weight, HT[i].weight);
}
// 从叶子结点开始构造哈夫曼编码
HC = new string[n + 1];
for (i = 1; i <= n; i++) {
int j, f; // 当前结点和父节点
for (j = i, f = HT[j].parent; f; j = f, f = HT[j].parent) {
if (HT[f].left == j)
HC[i] += '0';// 左子树,编码为0
else
HC[i] += '1'; // 右子树,编码为1
}
}
}
解码过程
-- 从根到叶子结点正向解码 --
int HuffmanDecoding(Htree HT, HuffmanCode HC,int c,int n) { //解码
/* * 输入码位置c(或者编码) * 返回解码出来的位置,如果相等表明正确 */
string code = HC[c];
int index = 2 * n - 1; // 代表最后是那个根结点
for (int i = code.size() - 1; i >= 0; i--) {
if (code[i] == '0') // 向左
index = HT[index].left;
else // 向右
index = HT[index].right;
}
return index;
}
测试用例
int main() {
Htree T;
HuffmanCode C;
int n = 8;
int w[] = {5,29,7,8,14,23,3,11};
HuffmanCoding(T, C, w, n);
// 打印编码结果
for (int i = 1; i <= n; i++) {
printf("#%d: 权重%d,编码%s\n", i, w[i - 1], C[i].c_str());
}
// 解码
for (int c = 1; c <= n; c++) {
printf("第%d位置的编码%s,解码%d\n", c, C[c].c_str(), HuffmanDecoding(T, C, c, n));
}
return 0;
}
参考文献
《数据结构 C语言描述》 严蔚敏著