目录
平衡二叉树
平衡二叉树的定义
- 为避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除二叉树结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(Balanced Binary Tree),简称平衡树。
- 定义结点左子树与右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是-1、0或1。因此,平衡二叉树可定义为或者是一棵空树,或者是具有下列性质的二叉树:
它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度差的绝对值不超过1。
平衡二叉树的插入
调 整 最 小 不 平 衡 子 树 A { L L 型 在 A 的 左 孩 子 的 左 子 树 中 插 入 导 致 不 平 衡 R R 型 在 A 的 右 孩 子 的 右 子 树 中 插 入 导 致 不 平 衡 L R 型 在 A 的 左 孩 子 的 右 子 树 中 插 入 导 致 不 平 衡 R L 型 在 A 的 右 孩 子 的 左 子 树 中 插 入 导 致 不 平 衡 调整最小不平衡子树A\begin{cases} LL型\;\;\;\;在A的左孩子的左子树中插入导致不平衡 \\ RR型\;\;\;\;在A的右孩子的右子树中插入导致不平衡 \\ LR型\;\;\;\;在A的左孩子的右子树中插入导致不平衡 \\ RL型\;\;\;\;在A的右孩子的左子树中插入导致不平衡 \\ \end{cases} 调整最小不平衡子树A⎩⎪⎪⎪⎨⎪⎪⎪⎧LL型在A的左孩子的左子树中插入导致不平衡RR型在A的右孩子的右子树中插入导致不平衡LR型在A的左孩子的右子树中插入导致不平衡RL型在A的右孩子的左子树中插入导致不平衡
LL型
- LL平衡旋转(右单旋转)。由于在结点A的左孩子 ( L ) (L) (L)的左子树 ( L ) (L) (L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。
- 将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
RR型
- RR平衡旋转(左单旋转)。由于在结点A的右孩子 ( R ) (R) (R)的右子树 ( R ) (R) (R)上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。
- 将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树。
代码思路
-
①LL型
-
实现f向右下旋转,p向右上旋转:
其中f是爹,p为左孩子,gf为f他爹
①f->lchild = p->rchild;
②p->rchild = f;
③gf->lchild/rchild = p;
BL<B<BR<A<AR -
②RR型
-
实现f向左下旋转,p向左上旋转:
其中d是爹,p为右孩子,gf为f他爹
①f->rchild = p->lchild;
②p->lchild = f;
③gf->lchild/rchild = p;
AL<A<BL<B<BR
左旋、右旋操作后可以保持二叉排序树的特性
LR型
- LR平衡旋转(先左后右双旋转)。由于在A的左孩子 ( L ) (L) (L)的右子树 ( R ) (R) (R)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。
- 先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置。
RL型
- RL平衡旋转(先右后左双旋转)。由于在A的右孩子 ( R ) (R) (R)的左子树 ( L ) (L) (L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。
- 先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后再把该C结点向左上旋转提升到A结点的位置。
哈夫曼树和哈夫曼编码
哈夫曼树的定义
-
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。
-
从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。
-
树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为 W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^nw_il_i WPL=∑i=1nwili,其中, w i w_i wi是第 i i i个叶结点所带的权值, l i l_i li是该叶结点到根结点的路径长度。
-
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树的构造
- 给定n个权值分别为 w 1 , w 2 , . . . , w n w_1,w_2,...,w_n w1,w2,...,wn的结点,构造哈夫曼树的算法描述如下:
- 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
- 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
- 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
- 重复步骤2)和3),直至F中只剩下一棵树为止
- 从上述构造过程中可以看出哈夫曼树具有如下特点:
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
- 构造过程中共新建了n-1个结点(双分支结点),因此哈夫曼树的结点总数为2n-1。
- 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。
- 哈夫曼树并不唯一,但WPL必然相同且为最优。
哈夫曼树的结点类型
typedef struct
{
char data; //结点值
double weight; //权重
int parent; //双亲结点
int lchild; //左孩子结点
int rchild; //右孩子结点
}HTNode;
哈夫曼树构造的算法
void CreatHT(HTNode ht[], int n0)
{
int i, k, lnode, rnode;
double min1, min2;
for (i = 0; i < 2 * n0 - 1; i++) ht[i].parent = ht[i].lchild = ht[i].rchild = -1; //所有结点的相关域置为-1
for (i = n0; i < 2 * n0 - 2; i++) //构造哈夫曼树的n0-1个分支节点
{
min1 = min2 = 32676; //lnode和rnode为最小权重的两个结点位置
lnode = rnode = -1;
for(k=0;k<i-1;k++) //在ht[0..i-1]中找权值最小的两个结点
if (ht[k].parent == -1) //只在尚未构造的二叉树的结点中查找
{
if (ht[k].weight < min1)
{
min2 = min1;
rnode = lnode;
min1 = ht[k].weight;
rnode = k;
}
else if (ht[k].weight < min2)
{
min2 = ht[k].weight;
rnode = k;
}
}
ht[i].weight = ht[lnode].weight + ht[rnode].weight;
ht[i].lchild = lnode;
ht[i].rchild = rnode;
ht[lnode].parent = i; //ht[i]作为双亲结点
ht[rnode].parent = i;
}
}
哈夫曼编码
- 在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。
- 可变长度编码比固定长度编码要好得多,其特点是对频率高的字符赋以短编码,而对频率较低的字符则赋以较长一些的编码,从而可以使字符的平均编码长度减短,起到压缩数据的效果。
- 哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
- 由哈夫曼树得到哈夫曼编码是很自然的过程。首先,将每个出现的字符当作一个独立的结点,其权值为它出现的频度(或次数),构造出对应的哈夫曼树。显然,所有字符结点都出现在叶结点中。我们可将字符的编码解释为从根至该字符的路径上边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。
- !注意:0和1究竟是表示左子树还是右子树没有明确规定。左、右孩子结点的顺序是任意的,所以构造出的哈夫曼树并不唯一,但各哈夫曼树的带权路径长度WPL相同且为最优。此外,如有若千权值相同的结点,则构造出的哈夫曼树更可能不同,但WPL必然相同且是最优的。
哈夫曼编码的结点类型
typedef struct
{
char cd[N]; //存放当前结点的哈夫曼编码
int start; //表示cd[start..n0]部分是哈夫曼码
}HCode;
哈夫曼编码的算法
void CreateCode(HTNode ht[], HCode hcd[], int n0)
{
int i, f, c;
HCode hc;
for (i = 0; i < n0; i++) //根据哈夫曼树求哈夫曼编码
{
hc.start = n0;
c = i;
f = ht[i].parent;
while (f != -1) //循环直到无双亲结点,即到达根结点
{
if (ht[f].lchild == c) hc.cd[hc.start--] = '0'; //当前结点是双亲结点的左孩子
else hc.cd[hc.start--] = '1'; //当前结点是双亲结点的右孩子
c = f;
f = ht[f].parent; //再对双亲结点进行同样的操作
}
hc.start++; //start指向哈夫曼编码最开始的字符
hcd[i] = hc;
}
}