本文内容源于对《数据结构(C语言版)》(严蔚敏著)和王道讲解的学习总结的笔记,以便考研复习。
主要针对 WPL 的算法进行解析,并扩展 WPL 的知识点,即求 WPL 最小的二叉树为哈夫曼树(即最优二叉树)的算法(构造哈夫曼树)。
可搭配以下链接一起学习:
【考研】常考的二叉树相关算法总结(详细全面)_住在阳光的心里的博客-CSDN博客
【考研】数据结构——线索二叉树_住在阳光的心里的博客-CSDN博客
【2023考研】数据结构常考应用典型例题(含真题)_住在阳光的心里的博客-CSDN博客
二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和。给定一棵二叉树,采用二叉链表存储,结点结构为
left weight right 其中叶结点的 weight 域保存该结点的非负权值。设 root 为指向 T 的根结点的指针,请设计求 T 的 WPL 的算法,要求:
(1)给出算法的基本设计思想。
(2)使用 C 或 C++ 语言,给出二叉树结点的数据类型定义。
(3)根据设计思想,采用 C 或 C++ 语言描述算法,关键之处给出注释。
本题考查二叉树的带权路径长度 WPL 。
WPL:每个叶结点的深度与权值之积的总和,可使用先序遍历或层次遍历解决问题。
(1)基于先序遍历
用一个 static 变量记录 wpl ,把每个结点的深度作为递归函数的一个参数传递,算法步骤如下:
(2)基于层次遍历
使用队列进行层次遍历,并记录当前的层数:
typedef struct BiTNode{
int weight; //权值
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
(1)基于先序遍历
int WPL(BiTree root){
return wpl_PreOrder(root, 0);
}
int wpl_PreOrder(BiTree root, int deep){
static int wpl = 0;
if(root->lchild == NULL && root->rchild == NULL) //若为叶结点,则累积 wpl
wpl += deep*root->weight;
if(root->lchild != NULL) //若左子树不空,则对左子树递归遍历
wpl_PreOrder(root->lchild, deep+1);
if(root->rchild != NULL) //若右子树不空,则对右子树递归遍历
wpl_PreOrder(root->rchild, deep+1);
return wpl;
}
(2)基于层次遍历
#define MaxSize 100 //设置队列的最大容量
int wpl_LevelOrder(BiTree root){
BiTree q[MaxSize]; //q 为队列,最多容纳 MaxSize - 1 个元素
int front = 0, rear = 0; //头指针front、尾指针rear分别指向队头元素和队尾的后一个元素
int wpl = 0, deep = 0; //初始化 wpl 和深度
//lastNode记录当前层的最后一个结点, newlastNode记录下一层的最后一个结点
BiTree lastNode, newlastNode;
lastNode = root; // lastNode 初始化为根结点
newlastNode = NULL; // newlastNode 初始化为空
q[rear++] = root; //根结点入队
while(front != rear){ //层次遍历,若队列不空则循环
BiTree t = q[front++]; //拿出队列中的头一个元素
if(t->lchild == NULL && t->rchild == NULL) //若为叶结点,统计wpl
wpl += deep * t->weight;
if(t->lchild != NULL){ //若为非叶结点,则把左结点入队
q[rear++] = t->lchild;
newlastNode = t->lchild; //设下一层的最后一个结点为该结点的左结点
}
if(t->rchild != NULL){ //若为非叶结点,则把右结点入队
q[rear++] = t->rchild;
newlastNode = t->rchild;
}
if(t == lastNode){ //若该结点为本层最后一个结点,则更新lastNode
lastNode = newlastNode;
deep += 1; //层数加一
}
}
return wpl;
}
(1)初始化。
首先动态申请 2n 个单元;
然后循环 2n - 1 次,从 1 号单元开始,依次将 1 至 2n - 1 所有单元中的双亲、左孩子、右孩子下标都初始化为 0;
最后再循环 n 次,输入前 n 个单元中叶子结点的权值。
(2)创建树。
循环 n - 1 次,通过 n - 1 次的选择、删除与合并来创建哈夫曼树。
选择是从当前森林中选择双亲为 0 且权值最小的两个树根结点 s1 和 s2;
删除是指将结点 s1 和 s2 的双亲改为非 0;
合并就是将 s1 和 s2 的权值和作为一个新结点的权值依次存入到数组的第 n + 1 之后的单元中,同时记录这个新结点左孩子的下标为 s1,右孩子的下标为 s2。
typedef struct{
int weight;
int parent, lchild, rchild;
}HTNode, *HuffmanTree;
//构造哈夫曼树 HT
void CreateHuffmanTree(HuffmanTree &HT, int n){
if(n <= 1) return;
int m = 2*n - 1;
HT = new HTNode[m+1]; //0号单元未用,所以需动态分配 m+1 个单元,HT[m]表示根结点
for(int i = 1; i <= m; ++i){ 将 1 至 m 号单元中的双亲、左右孩子的下标都初始化为 0
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
for(int j = 1; j <= n; ++j){
cin >> HT[i].weight; //输入前 n 个单元中叶子结点的权值
}
//通过 n - 1 次的选择、删除与合并来创建哈夫曼树
for(i = n + 1; i <= m; ++i){
//在 HT[k] 中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
Select(HT, i - 1, s1, s2);
HT[s1].parent = i; //得到新结点i, 从森林中删除s1和s2,将s1和s2的双亲域由0改为i
HT[s2].parent = i;
HT[i].lchild = s1; //s1、s2分别作为 i 的左右孩子
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight; // i 的权值为左右孩子权值之和
}
}