概念
- 路径:在一棵树中,从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径
- 路径长度:在一棵树中,从一个结点到另一个结点所经过的“边”的数量,被我们称为两个结点之间的路径长度。
- 结点的带权路径长度:树的根结点到该结点的路径长度和该结点权重的乘积
- 树的带权路径长度:在一棵树中,所有叶子结点的带权路径长度之和,被称为树的带权路径长度,也被简称为WPL。
计算方法
问题描述:给定树T,有n个叶结点,并且其权重值为{A1,A2,A3...An};如何计算树T的WPL
例如2021年408这道题
若某二叉树有5个叶子结点,其权值分别为10,12,16,21,30。则其最小的带权路径长度(WPL)是()
方法1
- 按照算法步骤画出哈夫曼树 具体算法如下:
1. 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点)
2. 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
3. 从森林中删除选取的两棵树,并将新树加入森林
4. 重复2、3步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树
举例说明
- 首先对集合进行排序得到
{10,12,16,21,30} - 我们找到权值最小的两个结点10和12合并;得到新的森林根结点为22。现在结点集合为
{16,21,22,30}
- 接着我们找到当前最小的结点16和21合并:得到新的森林根结点为37。现在结点集合为
{22,30,37}
- 接着我们找到当前最小的结点22和30合并:得到新的森林根结点为53。现在结点集合为
{37,53}
- 接着我们找到当前最小的结点37和53合并:得到新的森林根结点为90。现在结点集合为
{90};由于结点个数只剩一个,所以算法结束、构造哈夫曼树完毕
可以看到哈夫曼树的构造堆左右子树的顺序是没有要求的,当然我们画哈夫曼树的可以按照一定规律来这样更明确思路更清晰,比如我这里是按照
左节点<右结点的原则来画的
- 依次累加计算所有叶结点的带权路径长度 从上面构造的哈夫曼树可知所有结点的路径长度,例如结点”16“的路径长度为2
所以WPL=(16+21+30)*2+(10+12)*3=200
方法2
- 按照算法步骤画出哈夫曼树:步骤同方法1
- 将所有非根结点的权值累加起来:
WPL=37+52+16+21+22+30+10+12=200
这里我简单证明下上述结论:
- 对于哈夫曼树T1中的两个兄弟叶结点N1、N2,假设N1、N2的父结点为P1、N1的路径长度为d
- 删除结点N1、N2得到的哈夫曼树为T2
因为
W(N1)+W(N2)=N1+N2+(N1+N2)*(d-1)得到W(P1)=P1*(d-1)=W(N1)+W(N2)+N1+N2所以W(T1)=W(T2)+N1+N2;按照哈夫曼树构造过程,可知WPL(T1)=N1+N2+N3......+N,即WPL为所有叶结点的权值之和
方法3
不画哈夫曼树直接计算WPL
按照方法2的过程,我们可以每次找到最小两个结点后,直接累加到WPL里递归计算WPL
即WPL(T1)=min(n1+n2)+WPL(T2)
过程如下:
- 首先对集合进行排序得到
{10,12,16,21,30}一开始WPL=0 - 去掉最小的两个结点得到
{16,21,22,30},WPL=10+12=22 - 去掉最小的两个结点得到
{22,30,37},WPL=22+(16+21)=59 - 去掉最小的两个结点得到
{37,52},WPL=59+(22+30)=111 - 只剩两个结点了,直接累加,
WPL=111+(37+52)=200
代码实现
参考方法3,我们可以直接使用递归实现最简单
JS实现
/**
*
* @param arr 叶结点权值
* @return {number|*} 哈夫曼树带权路径长度
*/
function getHuffmanWpl (arr) {
if (arr.length === 0) {
return 0;
}
if (arr.length === 1) {
// 只有一个元素时直接返回其值
return arr[0];
}
if (arr.length === 2) {
return arr[0] + arr[1];
}
// 这里可以优化下,类似冒泡排序的效果把第一个数冒泡到对应排序的位置
arr.sort((v1, v2) => {
return v1 - v2
});
let sum = arr[0] + arr[1];
// 数组去掉前两个数+sum合并成新的数组,递归计算带权路径长度
return sum + getHuffmanWpl([sum, ...arr.splice(2)]);
}
C语言实现
/**
*
* @param arr 数组内容
* @param first 数组开始下标
* @param n 数组长度
* @return WPL值
*/
int getHuffmanWpl(int arr[], int first, int n) {
if (n - first == 0) {
return 0;
}
if (n - first == 1) {
// 只有一个元素时直接返回其值
return arr[0];
}
if (n - first == 2) {
return arr[first] + arr[first + 1];
}
int sum = arr[first] + arr[first + 1];
// 将第二个数改成sum
arr[first + 1] = sum;
int temp, i;
// 范围为[first+1,n]的数组排序:因为数组后面的数都已经排序,只需要把first+1位置的数冒泡到对应的位置即可
for (i = first + 1; i < n - 1; i++) {
if (arr[i + 1] < sum) {
arr[i] = arr[i + 1];
} else {
break;
}
}
arr[i] = sum;
//递归计算带权路径长度
return sum + getHuffmanWpl(arr, first + 1, n);
}
int getWpl(int arr[], int n) {
// 先对arr进行冒泡排序
for(int i = 0; i < n; i++) {
for(int j = 0, size = n - 1 - i; j < size; j++) {
if(arr[j + 1] < arr[j]) {
int tmp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tmp;
}
}
}
return getHuffmanWpl(arr, 0, n);
}