我们希望建立这样一株二叉树,其叶结点为一组给定的带权结点,称这个树的权重为每个叶子结点到根结点的距离与其权值的乘积的累和,即
$$ T.w=sum_{x is leaf of T}^{}{x.wcdot x.d} $$
其中x.w表示叶结点的权重,而x.d为叶结点的深度。哈夫曼树是所有满足上面条件的二叉树中权重最小的。
下面说明一些关于哈夫曼树的命题:
命题1:哈夫曼树是满二叉树。
证明:这是显然的,因为若哈夫曼树不是满二叉树,即存在顶点p,其只有一个子结点。那么我们可以利用p的唯一子结点替换p,这样就可以保证其下的所有叶结点的深度减少1,从而减少树的权值。这与哈夫曼树的定义相悖,故命题成立。
命题2:哈夫曼树中权重较小的叶结点的深度不可能小于权重较大的叶结点的深度。
证明:如果情况发生,交换两个叶结点的位置,即可降低树的权重。
命题3:存在这样一株哈夫曼树,其中权重最小的两个叶结点的父结点相同。
证明:设权重最小的叶结点为x,而第二小的为y。由命题1知,x的兄弟结点是存在的。而由命题2知,叶结点的深度是按权重值非严格递减的。因此我们能保证y的深度与x一致,对于任意哈夫曼树,我们只需要令y和x的兄弟的位置交换即可得到目标哈夫曼树。
下面我们给出构建哈夫曼树的过程。我们将所有叶结点加入一个最小堆中,只要其中至少有两个结点,我们就弹出其中权重最小的两个顶点, 并创建一个新结点,其两个子结点为方才弹出的两个结点。之后我们将新建的结点加入堆中。不断循环上面过程,直到最终只余下一个顶点,此时余下的顶点即为哈夫曼树的根结点。
buildHfmTree(nodeList) heap = an-empty-min-heap for node in nodeList heap.add(node) while(heap.size() >= 2) father = a-new-node father.left = heap.pop() father.right = heap.pop() father.w = father.left.w + father.right.w heap.add(father) return heap.pop()
简直难以想象,这么简短的代码就可以实现哈夫曼树。
算法的时间复杂度为O(nlog2(n)),其中n为叶结点数。
下面说明算法的正确性:通过归纳法说明,当只有一个顶点时,算法显然正确,此时唯一的叶结点就是根结点,我们将其返回,而此时哈夫曼树的权值达到了最小,为0。当顶点数少于k时,若上述算法都可以构建一株合理的哈夫曼树。那么当我们持有k个结点组成的结点集合V时,由于命题3我们已知权重最小的两个结点x,y可以有相同的父亲f,我们利用上述算法,使用k-1个结点组成的结点集合V'(V中移除了x与y后加入f得到)建立对应的一株哈夫曼树F。假设T为V的哈夫曼树。我们可以在T的基础上建立一株新树T',其中T'与T的区别在于我们为f赋予权值x.w+y.w,同时从T中删除x与y,显然T'.w = T.w - x.w - y.w。而T'也是满足以V'为叶结点的一株二叉树,故知F.w<=T'.w=T.w-x.w-y.w。同样我们可以在F的基础上建立另外一株二叉树F',其中F'与F的区别在于我们移除f结点的权值,并为其添加两个子结点x与y,此时显然有F'.w=F.w+x.w+y.w,而由于F'.w是V的一株二叉树,因此T.w<=F'.w=F.w+x.w+y.w。结合两条不等式可以得出F.w+x.w+y.w=T.w,即在F的基础上做改变得到的树F'是V的哈夫曼树。因此我们可以通过递归的思路建立哈夫曼树,而上述的代码正是递归展开成为循环后的样子。