我们采用反证法来证明该命题。假设在一棵最优前缀编码树中,存在两个权值最小的叶结点,它们不是最深的叶结点。设这两个结点为 a 和 b,且它们的深度小于某个最深的叶结点。对于这个最深的叶结点 c,我们可以将 a 和 c 交换位置,或将 b 和 c 交换位置。由于 Huffman 算法保证树的每一层按权值最小的叶结点合并,因此在交换后,树的带权路径长度(WPL)将减少。由此矛盾可得出假设不成立,因此权值最小的两个叶结点必须是最深的叶结点。
接下来,假设这两个权值最小的叶结点分别为 a 和 b,它们的深度相同。如果在一棵最优前缀编码树中这两个结点不是兄弟结点,假设存在其他结点 c 和 d 与 a 和 b 分别是兄弟结点(假设 a 和 c 是兄弟结点,b 和 d 是兄弟结点)。我们可以将 a 和 b 合并为一个子树。
如果 a 和 b 合并后的权值之和小于 c 或 d 的权值,那么我们可以将合并后的子树与权值较大的结点(如 c 或 d)合并,形成新的子树,WPL 会减少。
如果 a 和 b 的权值之和不小于 c 和 d 的权值,我们可以直接将 a 和 b 调整为兄弟结点,c 和 d 作为另一个兄弟结点,WPL 不会增加。
因此,经过这样的调整,最优性不会被破坏,得证。
定理
Huffman 算法得到的前缀编码树是最优前缀编码树。
证明
我们使用数学归纳法来证明该定理。
基本情况: 当字母数 n=2 时,显然,直接将两个字母合并成一棵树即为最优编码树。
归纳假设: 假设对于字母数 n=k(k≥2)时,Huffman 算法能够得到最优前缀编码树。
归纳步骤: 对于字母数 n=k+1,我们从 k+1 个字母中选出两个权值最小的字母,将它们合并为一棵子树,子树的根作为虚拟字母(虚拟结点)。根据引理可知,这一操作不会破坏前缀编码树的最优性。此时,虚拟字母与剩下的 k 个字母一同构成 k+1 个字母,根据归纳假设,当字母数为 k 时,Huffman 算法能够得到最优前缀编码树。
因此,通过数学归纳法,Huffman 算法对于任意字母数 n 都能够得到最优前缀编码树,得证。
霍夫曼编码
在进行程序设计时,通常给每一个字符标记一个单独的代码来表示一组字符,即 编码。
在进行二进制编码时,假设所有的代码都等长,那么表示 n 个不同的字符需要 ⌈log2n⌉ 位,称为 等长编码。
struct HNode { int weight; HNode *lchild, *rchild;};using Htree = HNode *;Htree createHuffmanTree(int arr[], int n) { Htree forest[N]; Htree root = NULL; for (int i = 0; i < n; i++) { // 将所有点存入森林 Htree temp; temp = (Htree)malloc(sizeof(HNode)); temp->weight = arr[i]; temp->lchild = temp->rchild = NULL; forest[i] = temp; } for (int i = 1; i < n; i++) { // n-1 次循环建哈夫曼树 int minn = -1, minnSub; // minn 为最小值树根下标,minnsub 为次小值树根下标 for (int j = 0; j < n; j++) { if (forest[j] != NULL && minn == -1) { minn = j; continue; } if (forest[j] != NULL) { minnSub = j; break; } } for (int j = minnSub; j < n; j++) { // 根据 minn 与 minnSub 赋值 if (forest[j] != NULL) { if (forest[j]->weight < forest[minn]->weight) { minnSub = minn; minn = j; } else if (forest[j]->weight < forest[minnSub]->weight) { minnSub = j; } } } // 建新树 root = (Htree)malloc(sizeof(HNode)); root->weight = forest[minn]->weight + forest[minnSub]->weight; root->lchild = forest[minn]; root->rchild = forest[minnSub]; forest[minn] = root; // 指向新树的指针赋给 minn 位置 forest[minnSub] = NULL; // minnSub 位置为空 } return root;}
计算构成霍夫曼树的 WPL
struct HNode { int weight; HNode *lchild, *rchild;};using Htree = HNode *;int getWPL(Htree root, int len) { // 递归实现,对于已经建好的霍夫曼树,求 WPL if (root == NULL) return 0; else { if (root->lchild == NULL && root->rchild == NULL) // 叶节点 return root->weight * len; else { int left = getWPL(root->lchild, len + 1); int right = getWPL(root->rchild, len + 1); return left + right; } }}
对于未建好的霍夫曼树,直接求其 WPL
int getWPL(int arr[], int n) { // 对于未建好的霍夫曼树,直接求其 WPL priority_queue<int, vector<int>, greater<int>> huffman; // 小根堆 for (int i = 0; i < n; i++) huffman.push(arr[i]); int res = 0; for (int i = 0; i < n - 1; i++) { int x = huffman.top(); huffman.pop(); int y = huffman.top(); huffman.pop(); int temp = x + y; res += temp; huffman.push(temp); } return res;}
对于给定序列,计算霍夫曼编码
struct HNode { int weight; HNode *lchild, *rchild;};using Htree = HNode *;void huffmanCoding(Htree root, int len, int arr[]) { // 计算霍夫曼编码 if (root != NULL) { if (root->lchild == NULL && root->rchild == NULL) { printf("结点为 %d 的字符的编码为: ", root->weight); for (int i = 0; i < len; i++) printf("%d", arr[i]); printf("\n"); } else { arr[len] = 0; huffmanCoding(root->lchild, len + 1, arr); arr[len] = 1; huffmanCoding(root->rchild, len + 1, arr); } }}