上文分析了哈夫曼树的构造方式:
机器学习入坑者:一文搞懂如何构造哈夫曼树?
下面从C++实现的角度对构建哈夫曼树的过程进行分析。首先给出创建哈夫曼树的完整步骤如下,每个步骤的具体分析见下文:
创建容量为2n-1的静态数组,元素为节点
对数组所有2n-1个元素(节点)进行初始化,并对前n个叶子节点赋予权值,后n-1个节点为空
查找数组中权值不为零且无双亲节点,选择其中权值最小的两个节点
对上一步获得的两个节点进行合并,生成新的节点
上一步参与合并的两个节点不再参与后续合并(设置parent指向新节点),新生成的节点存到数组下一个空位置,空位置索引向后移动一位
重复3、4、5共n-1次,最后一次获得的节点即为哈夫曼树的根节点
如何存储哈夫曼树?
当集合T元素(叶子节点)个数确定以后,可以使用数组容量的计算法则确定数组大小,所以可采用静态数组实现哈夫曼树的构建。哈夫曼树节点包括:
权值(用户指定);
左右孩子;
parent(标志其是否为根节点);
如何确定静态数组长度?
从前面的分析可知,可采用静态数组存储哈夫曼树,下面是确定静态数组长度的流程:
(1)在构建哈夫曼树之前,共有n个树(叶子节点),构成集合T,需要数组程度为n;
(2)每次进行合并时只有两个树参与,生成一个新树(需要给数组增加一个元素),此时需要数组长度为n+1;
(3)每次合并后,集合T元素个数减少一个(参与合并的两棵树从T中去除,新生成的树加入T);
(4)构造完成过后只有1个树,即T只剩一个元素,所以一共进行了n-1次构造,生成了n-1个新树,即数组元素个数从原来的n变为n+(n-1) = 2n-1个。
备注:上述的每个“树”只需要一个节点存储即可,也就是只需要使用数组中的一个元素进行存储,即结合T中的每棵树只需要存储其根节点。
如何设计节点类并初始化?
根据前面的分析可知,节点类需包含左右孩子、权值和parent节点位置,定义如下:
class Node {
public:
int left, right;
// -1表示无父节点
int parent = -1;
int weight = 0;
};
对数组中前n个树的根节点进行权值初始化的函数如下:
void initLeafs(Node *p, int numLeafs) {
// 对p指向的数组中前numLeafs个元素进行初始化
for (int i = 0; i < numLeafs; i++) {
int ch;
cout << "input : " << endl;
cin >> ch;
// 设置Node的权重
p[i].weight = ch;
}
cout << “---------------” << endl;
}
如何查找数组中权值最小且无parent的两个节点?
查找权值最小的两个根节点时,需要保证该根节点无parent。合并操作是针对根节点进行的,有parent的节点不会是树的根节点。搜索所有已经创建的根节点中权值最小的两个节点代码如下,可以认为是一种按序搜索算法:
void searchMin(const Node *p, int nums, int &min1, int &min2) {
// 在p指向的nums个元素的数组中
// 搜索权值最小的两个节点
// 并保证其parent为空,即根节点
// 首先找到数组中第一个根节点,即parent为空的节点
// 将此根节点作为weight最小节点,即采用排序算法
for (int i = 0; i < nums; i++) {
if (p[i].parent == -1) {
min1 = i;
break;
}
}
// 寻找数组中权值最小的根节点作为min1
for (int i = 0; i < nums; i++) {
if (p[i].parent == -1 && p[i].weight < p[min1].weight) {
min1 = i;
}
}
// 寻找到所有根节点中权值最小的weight1以后,继续寻找第二个
// 找到数组中第一个根节点,作为初始权值最小的位置min2
// 必须保证第min2不等于min1
for (int i = 0; i < nums; i++) {
if (p[i].parent == -1 && i != min1) {
min2 = i;
break;
}
}
// 寻找数组中除去min1以外权值最小的根节点
for (int i = 0; i < nums; i++) {
if (p[i].parent == -1 && p[i].weight < p[min2].weight && i != min1) {
min2 = i;
}
}
}
如何进行合并操作?
进行两颗树合并时,首先要保证两棵树的根节点权值是所有根节点中最小的,然后将参与合并的两个根节点权值相加,形成新的根节点(新的树)。最后,将新的根节点插入到数组中,并将合并的两个根节点的parent设置为1(标志这两个根节点不再是根节点,而是树的子树了,以后不会再参与合并过程)。
合并过程共n-1次,对应的创建哈夫曼树的总体代码如下:
void createHuffman(Node *p, int numLeafs) {
// 1、初始化前numLeafs个根节点,指定其权值
initLeafs(p, numLeafs);
// 创建用于存储权值最小两个节点的位置
int minPos1, minPos2;
// 2、numLeafs个根节点需要进行numLeafs-1次合并
for (int i = 0; i < numLeafs - 1; i++) {
// 3、寻找权值最小的前两个节点
// 寻找的范围是前numLeafs+i个节点
// 即所有已经创建的节点
searchMin(p, numLeafs + i, minPos1, minPos2);
// 4、在numLeafs+1处创建新根节点
p[numLeafs + i].weight = p[minPos1].weight + p[minPos2].weight;
p[numLeafs + i].left = minPos1;
p[numLeafs + i].right = minPos2;
// 将当前权值最小的两个节点的parent指向新创建的根节点
// 表示其不再是根节点,不再参与合并
p[minPos1].parent = numLeafs + i;
p[minPos2].parent = numLeafs + i;
}
}
如何验证哈夫曼树创建成功?
通过输出哈夫曼树所有节点的全部信息,可以查看节点权值、左右孩子位置、父节点位置,进而确保代码无误:
void printHuffman(Node *p, int numNodes) {
// 输出所有的节点,及节点信息
for (int i = 0; i < numNodes; i++) {
cout << setw(8) << p[i].weight <<
setw(8) << p[i].parent <<
setw(8) << p[i].left <<
setw(8) << p[i].right << endl;
}
}