数据结构与算法专栏 —— C++实现

写在前面:
这一讲我们来学习一个比较有趣的树 —— 哈夫曼树,在许多非常知名的算法里也出现了哈夫曼树,这一讲我们就好好来唠唠什么是哈夫曼树。

前置概念

概念一:什么是结点路径的长度

从根结点到该结点的路径上的连接数。
在这里插入图片描述
例如上图(下面将会用到),从结点 28 到结点 2 的连接数为 3 。

概念二:什么是树的路径长度

就是树的每个叶子结点的路径长度之和,上图的树的路径长度就为 3 + 3 + 3 + 3 + 1 = 13 。

概念三:什么是结点的带权路径长度

结点的路径长度与结点权值的乘积。

概念四:什么是树的带权路径长度 WPL

就是树的所有叶子结点的带权路径长度之和。

应用场景

在数据膨胀、信息爆炸的今天,数据压缩的意义不言而喻。谈到数据压缩,就不能不提赫夫曼(Huffman)编码,赫夫曼编码是首个实用的压缩编码方案,即使在今天的许多知名压缩算法里,依然可以见到赫夫曼编码的影子。

另外,在数据通信中,用二进制给每个字符进行编码时不得不面对的⼀个问题是如何使电稳总长最短且不产生二义性。根据字符出现频率,利用赫夫曼编码可以构造出一种不等长的二进制,使编码后的电文长度最短,且保证不产生二义性。

当有了以上概念以后,我们就可以用WPL去判断一颗二叉树的性能。WPL的值越小,二叉树的性能越优。

哈夫曼树的构造

(1)给出我们要构造的结点权值。
在这里插入图片描述
(2)找出两个最小的权值,创建双亲结点,并且其权值等于这两个最小的权值结点之和。将这两个结点移出后续的大小判断,并将新创建的结点加入后续的大小判断。
在这里插入图片描述
(3)不断地重复上述操作。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//找到两个最小的权值(这里为了方便大家理解,就分开找两个最小值了)
void select(HuffmanTree huffmanTree[], int n, int &s1, int &s2) {
	int min;
	//遍历全部的结点,找出一个单结点
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0) {
			min = i;
			break;
		}
	}
	//继续遍历全部结点,找出权值最小的单结点
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0) {
			if (huffmanTree[i].weight < huffmanTree[min].weight) {
				min = i;
			}
		}
	}
	s1 = min;
	//进行和上面相同的操作,找到第二小的结点
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0 && i != s1) {
			min = i;
			break;
		}
	}
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0 && i != s1) {
			if (huffmanTree[i].weight < huffmanTree[min].weight) {
				min = i;
			}
		}
	}
	s2 = min;
}

//构建哈夫曼树
void createHuffmanTree(int w[], int n) {
	int s1;
	int s2;
	int m = 2 * n - 1;
	//1--n号空间存放叶子结点,初始化结点
	for (int i = 1; i <= n; i++) {
		//其中叶子结点的权值是w[n]数组保存
		huffmanTree[i].weight = w[i];
		huffmanTree[i].lChild = 0;
		huffmanTree[i].rChild = 0;
		huffmanTree[i].parent = 0;
	}
	//对于其它的结点即非叶子结点
	for (int i = n + 1; i <= m; i++) {
		huffmanTree[i].weight = w[i];
		huffmanTree[i].lChild = 0;
		huffmanTree[i].rChild = 0;
		huffmanTree[i].parent = 0;
	}

	//创建非叶子结点,构建哈夫曼树
	for (int i = n + 1; i <= m; i++) {
		//找到权值最小的两个结点,分别赋值给s1和s2
		select(huffmanTree, i - 1, s1, s2);
		huffmanTree[s1].parent = i;
		huffmanTree[s2].parent = i;
		huffmanTree[i].lChild = s1;
		huffmanTree[i].rChild = s2;
		huffmanTree[i].weight = huffmanTree[s1].weight + huffmanTree[s2].weight;
	}
}

哈弗曼编码

我们将结点到其左孩子的路径上标记为 0 ,将结点到其右孩子的路径上标记为 1 ,这样就可以得到一个编码树。
在这里插入图片描述
通过上述操作,就可以得到每个结点对应的编码,结果如下:

  • 2:0 0 0
  • 3:0 0 1
  • 4:0 1 0
  • 4:0 1 1(这里权值为 4 的结点有两个)
  • 15:1

通过观察可以发现,每个结点的编码都具有前缀性,即没有任何编码是其他编码的前缀,也就是说每一个叶子结点不能出现在到另一个叶子结点的路径上。

//从n个叶子结点到根求哈弗曼编码
void creatHuffmanCode(int n) {
	string *huffmanCode = new string[n + 1];	//创建一个储存编码的数组
	for (int i = 1; i <= n; i++) {
		string cd = "";
		//这里我们从叶子结点开始向上遍历
		for (int c = i, p = huffmanTree[i].parent; p != 0; c = p, p = huffmanTree[p].parent) {
			if (huffmanTree[p].lChild == c) {
				cd = "0" + cd;	//如果该结点是双亲结点的左孩子,则在编码前加‘0’
			} else {
				cd = "1" + cd;	//如果该结点是双亲结点的右孩子,则在编码前加‘1’
			}
		}
		huffmanCode[i] = cd;
	}
	//输出编码
	for (int i = 1; i <= n; i++) {
		cout << huffmanTree[i].weight << " " << huffmanCode[i] << endl;
	}
}

全部代码

#include <bits/stdc++.h>
using namespace std;

struct HuffmanTree {
	int weight;	//权值
	int lChild;	//左孩子
	int rChild;	//右孩子
	int parent;	//双亲结点
};

HuffmanTree *huffmanTree = NULL;
void createHuffmanTree(int[], int);
void creatHuffmanCode(int);

int main() {
	int n;
	int w[1000];
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> w[i];
	}
	int m = 2 * n - 1;
	huffmanTree = new HuffmanTree[m + 1];
	createHuffmanTree(w, n);
	creatHuffmanCode(n);
}

//找到两个最小的权值(这里为了方便大家理解,就分开找两个最小值了)
void select(HuffmanTree huffmanTree[], int n, int &s1, int &s2) {
	int min;
	//遍历全部的结点,找出一个单结点
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0) {
			min = i;
			break;
		}
	}
	//继续遍历全部结点,找出权值最小的单结点
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0) {
			if (huffmanTree[i].weight < huffmanTree[min].weight) {
				min = i;
			}
		}
	}
	s1 = min;
	//进行和上面相同的操作,找到第二小的结点
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0 && i != s1) {
			min = i;
			break;
		}
	}
	for (int i = 1; i <= n; i++) {
		if (huffmanTree[i].parent == 0 && i != s1) {
			if (huffmanTree[i].weight < huffmanTree[min].weight) {
				min = i;
			}
		}
	}
	s2 = min;
}

//构建哈夫曼树
void createHuffmanTree(int w[], int n) {
	int s1;
	int s2;
	int m = 2 * n - 1;
	//1--n号空间存放叶子结点,初始化结点
	for (int i = 1; i <= n; i++) {
		//其中叶子结点的权值是w[n]数组保存
		huffmanTree[i].weight = w[i];
		huffmanTree[i].lChild = 0;
		huffmanTree[i].rChild = 0;
		huffmanTree[i].parent = 0;
	}
	//对于其它的结点即非叶子结点
	for (int i = n + 1; i <= m; i++) {
		huffmanTree[i].weight = w[i];
		huffmanTree[i].lChild = 0;
		huffmanTree[i].rChild = 0;
		huffmanTree[i].parent = 0;
	}

	//创建非叶子结点,构建哈夫曼树
	for (int i = n + 1; i <= m; i++) {
		//找到权值最小的两个结点,分别赋值给s1和s2
		select(huffmanTree, i - 1, s1, s2);
		huffmanTree[s1].parent = i;
		huffmanTree[s2].parent = i;
		huffmanTree[i].lChild = s1;
		huffmanTree[i].rChild = s2;
		huffmanTree[i].weight = huffmanTree[s1].weight + huffmanTree[s2].weight;
	}
}

//从n个叶子结点到根求哈弗曼编码
void creatHuffmanCode(int n) {
	string *huffmanCode = new string[n + 1];	//创建一个储存编码的数组
	for (int i = 1; i <= n; i++) {
		string cd = "";
		//这里我们从叶子结点开始向上遍历
		for (int c = i, p = huffmanTree[i].parent; p != 0; c = p, p = huffmanTree[p].parent) {
			if (huffmanTree[p].lChild == c) {
				cd = "0" + cd;	//如果该结点是双亲结点的左孩子,则在编码前加‘0’
			} else {
				cd = "1" + cd;	//如果该结点是双亲结点的右孩子,则在编码前加‘1’
			}
		}
		huffmanCode[i] = cd;
	}
	//输出编码
	for (int i = 1; i <= n; i++) {
		cout << huffmanTree[i].weight << " " << huffmanCode[i] << endl;
	}
}

如果大家有什么问题的话,欢迎在下方评论区进行讨论哦~

【上一讲】树 - 05 线索二叉树
【下一讲】树 - 07 平衡二叉树

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐