哈夫曼树带权路径长度(WPL)计算

3,100 阅读3分钟

概念

  1. 路径:在一棵树中,从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径
  2. 路径长度:在一棵树中,从一个结点到另一个结点所经过的“边”的数量,被我们称为两个结点之间的路径长度。
  3. 结点的带权路径长度:树的根结点到该结点的路径长度和该结点权重的乘积
  4. 树的带权路径长度:在一棵树中,所有叶子结点的带权路径长度之和,被称为树的带权路径长度,也被简称为WPL。

计算方法

问题描述:给定树T,有n个叶结点,并且其权重值为{A1,A2,A3...An};如何计算树T的WPL

例如2021年408这道题

若某二叉树有5个叶子结点,其权值分别为1012162130。则其最小的带权路径长度(WPL)是()

方法1

  1. 按照算法步骤画出哈夫曼树 具体算法如下:
1. 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点)
2. 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
3. 从森林中删除选取的两棵树,并将新树加入森林
4. 重复2、3步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树

举例说明

  • 首先对集合进行排序得到{10,12,16,21,30}
  • 我们找到权值最小的两个结点10和12合并;得到新的森林根结点为22。现在结点集合为{16,21,22,30}

graph1.png

  • 接着我们找到当前最小的结点16和21合并:得到新的森林根结点为37。现在结点集合为{22,30,37}

graph2.png

  • 接着我们找到当前最小的结点22和30合并:得到新的森林根结点为53。现在结点集合为{37,53}

graph3.png

  • 接着我们找到当前最小的结点37和53合并:得到新的森林根结点为90。现在结点集合为{90};由于结点个数只剩一个,所以算法结束、构造哈夫曼树完毕

graph4.png

可以看到哈夫曼树的构造堆左右子树的顺序是没有要求的,当然我们画哈夫曼树的可以按照一定规律来这样更明确思路更清晰,比如我这里是按照左节点<右结点的原则来画的

  1. 依次累加计算所有叶结点的带权路径长度 从上面构造的哈夫曼树可知所有结点的路径长度,例如结点”16“的路径长度为2

所以WPL=(16+21+30)*2+(10+12)*3=200

方法2

  1. 按照算法步骤画出哈夫曼树:步骤同方法1
  2. 将所有非根结点的权值累加起来: WPL=37+52+16+21+22+30+10+12=200

这里我简单证明下上述结论:

  1. 对于哈夫曼树T1中的两个兄弟叶结点N1、N2,假设N1、N2的父结点为P1、N1的路径长度为d
  2. 删除结点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);
}