6.1 定义
树(Tree)是n(n>=0)个结点的有限集。n=0时为空树。
在任意一棵非空树中:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
- 该定义设计到递归的方法
6.1.1 相关概念
- 度(Degree)
- 叶结点(Leaf)
- 分支结点
- 孩子
- 双亲
- 兄弟
- 层次
- 深度
- 有序树/无序树
- 森林
6.2 存储结构
6.2.1 双亲表示法
根据不同需要,设置不同存储结构
- 双亲表示
下标 | data | parent |
---|---|---|
0 | A | -1 |
1 | B | 0 |
2 | C | 0 |
3 | D | 1 |
4 | E | 2 |
5 | F | 2 |
6 | G | 3 |
7 | H | 3 |
8 | I | 3 |
9 | J | 4 |
- 改进:添加长子域,如果树的度为2,可分清长子和次子。
下标 | data | parent | firstchild |
---|---|---|---|
0 | A | -1 | 1 |
1 | B | 0 | 3 |
2 | C | 0 | 4 |
3 | D | 1 | 6 |
4 | E | 2 | 9 |
5 | F | 2 | -1 |
6 | G | 3 | -1 |
7 | H | 3 | -1 |
8 | I | 3 | -1 |
9 | J | 4 | -1 |
- 改进:增加右兄弟,体现兄弟关系
下标 | data | parent | rightsib |
---|---|---|---|
0 | A | -1 | -1 |
1 | B | 0 | 2 |
2 | C | 0 | -1 |
3 | D | 1 | -1 |
4 | E | 2 | 5 |
5 | F | 2 | -1 |
6 | G | 3 | 7 |
7 | H | 3 | 8 |
8 | I | 3 | -1 |
9 | J | 4 | -1 |
6.2.2 孩子表示法
多重链表表示法:每个结点包含多个指针域,每个指针指向一个子树
6.2.2.1 两种方案
方案一:指针个数=树的度
方案二:增加位置存储当前节点指针域个数
6.2.2.2 具体方法
普通方法
改进方法(携带双亲信息)
6.2.1 孩子兄弟表示法(二叉树的来源)
-
结点机构
-
方法实现(将复杂的树变成一棵二叉树)
6.3 二叉树
6.3.1 二叉树定义
- 二叉树(Binary Tree)是 n (n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的右子树和左子树的二叉树组成。
(递归)
6.3.2 二叉树特点
- 度不大于2
- 左子树和右子树有顺序,次序不能颠倒
- 只有一棵子树,也要区分左右
6.3.3 特殊二叉树
- 斜树
- 满二叉树:每一层皆满
- 完全二叉树:与满二叉树的不同在于,完全二叉树最后一层不满且叶子结点集中在左部连续位置。
6.3.4 二叉树性质
性质1
- 在二叉树的第i层上至多有 2i-1个结点(i>=1)
性质2
- 深度为k的二叉树至多有2k-1个结点(k>=1)
- 推导过程:
等比和公式: S n = a 1 q k − 1 q − 1 S_n=a_1\frac{q^k-1}{q-1} Sn=a1q−1qk−1, a 1 a_1 a1 为等比首项,q为公比,k为等比项数。
性质3
- 对任何一棵二叉树T,如果其叶子结点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
- 推到过程:
(1)二叉树结点总数为 n 0 + n 1 + n 2 n_0+n_1+n_2 n0+n1+n2
(2)分支线总数为 n 0 × 0 + n 1 × 1 + n 2 × 2 n_0\times0+n_1\times1+n_2\times2 n0×0+n1×1+n2×2
(3)结点总数-1=分支线总数
(4) n 0 + n 1 + n 2 − 1 = n 1 + 2 n 2 n_0+n_1+n_2-1=n_1+2n_2 n0+n1+n2−1=n1+2n2 ⇒ \Rightarrow ⇒ n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
性质4
- 具有n个结点的完全二叉树的深度为 ⌊ log 2 n ⌋ + 1 \lfloor\log_2n\rfloor+1 ⌊log2n⌋+1 ( ⌊ X ⌋ \lfloor X\rfloor ⌊X⌋表示不大于X的最大整数)
- 推导过程:
(1)深度为k的满二叉树的结点数n一定是 2 k − 1 2^k-1 2k−1,即 n = 2 k − 1 n=2^k-1 n=2k−1 ⇒ \Rightarrow ⇒ k = log 2 ( n + 1 ) k=\log_2(n+1) k=log2(n+1)
(2)完全二叉树的节点数一定介于深度为k的满二叉树与深度为(k-1)的满二叉树之间,即: 2 k − 1 − 1 < n ⩽ 2 k − 1 2^{k-1}-1<n\leqslant2^k-1 2k−1−1<n⩽2k−1。
(3)n为整数,则 n > 2 k − 1 − 1 n>2^{k-1}-1 n>2k−1−1 ⇒ \Rightarrow ⇒ n ⩾ 2 k − 1 n\geqslant2^{k-1} n⩾2k−1, n ⩽ 2 k − 1 n\leqslant2^k-1 n⩽2k−1 ⇒ \Rightarrow ⇒ n < 2 k n<2^k n<2k ,即 2 k − 1 ⩽ n < 2 k 2^{k-1}\leqslant n<2^k 2k−1⩽n<2k ⇒ \Rightarrow ⇒ k − 1 ⩽ log 2 n < k k-1\leqslant\log_2n<k k−1⩽log2n<k。
(4) log 2 n \log_2n log2n必须为整数,所以 log 2 n = k − 1 \log_2n=k-1 log2n=k−1 ⇒ \Rightarrow ⇒ k = ⌊ log 2 n ⌋ + 1 k=\lfloor\log_2n\rfloor+1 k=⌊log2n⌋+1
性质5
- 如果对一棵有n个结点的完全二叉树(其深度为
⌊
log
2
n
⌋
+
1
\lfloor\log_2n\rfloor+1
⌊log2n⌋+1)的结点按层序编号(从第1层到第
⌊
log
2
n
⌋
+
1
\lfloor\log_2n\rfloor+1
⌊log2n⌋+1层,没层从左到右),对任一结点 i(
1
⩽
i
⩽
n
1\leqslant i\leqslant n
1⩽i⩽n)有:
1.如果 i=1,则结点 i 是二叉树的根,无双亲;如果i>1,则其双亲是节点 ⌊ i 2 ⌋ \lfloor\frac{i}{2}\rfloor ⌊2i⌋;
2.如果2i>n,则结点i无左孩子,结点i为叶子结点;否则其左孩子是结点2i;
3.如果2i+1>n,则节点i无右孩子;否则右孩子是结点2i+1。
6.3.5 二叉树存储结构
- 顺序存储:一维数组存储二叉树,数组下标体现节点间逻辑结构
- 二叉链表:
6.3.6 遍历二叉树
- 按照某种次序访问二叉树所有结点,是每个结点被访问一次且仅被访问一次
6.3.6.1 前序遍历
- 遍历顺序:中、左、右
/*二叉树的前序遍历递归算法*/
void PreOrderTraverse (BiTree T)
{
if (T == NULL)
return;
printf("%c",T->data);/*显示结点数据,可以更改为其他对结点的操作*/
PreOrderTraverse(T->lchild);/*再先序遍历左子树*/
PreOrderTraverse(T->rchild);/*最后先序遍历右子树*/
}
6.3.6.2 中序遍历
- 遍历顺序:左、中、右
/*二叉树的中序遍历递归算法*/
void InOrderTraverse (BiTree T)
{
if (T == NULL)
return;
InOrderTraverse(T->lchild);/*中序遍历左子树*/
printf("%c",T->data);/*再显示结点数据,可以更改为其他对结点的操作*/
InOrderTraverse(T->rchild);/*最后中序遍历右子树*/
}
6.3.6.3 后序遍历
- 遍历顺序:左、右、中
/*二叉树的后序遍历递归算法*/
void PostOrderTraverse (BiTree T)
{
if (T == NULL)
return;
PostOrderTraverse(T->lchild);/*后序遍历左子树*/
PostOrderTraverse(T->rchild);/*再后序遍历右子树*/
printf("%c",T->data);/*最后显示结点数据,可以更改为其他对结点的操作*/
}
6.3.6.4 推导遍历结果
- 已知前序(或后序)遍历序列和中序遍历序列,可以唯一确定一棵二叉树;
- 已知前序遍历序列和后续遍历序列,不能唯一确定一棵二叉树。
6.3.7 建立二叉树
- 与遍历代码相似,只不过将打印节点的地方,改成了生成结点、给结点赋值的操作而已。
/*按前序输入二叉树中节点的值(一个字符)*/
/* #表示空树,构造二叉链表表示二叉树T。*/
void CreateBiTree (BiTree *T)
{
TElemType ch;
scanf("%c",&ch); /*使用`scanf`函数从标准输入(键盘输入)中读取一个字符*/
if (ch == '#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode)); /*使用malloc函数分配内存来创建新的节点*/
if(!*T) /*内存分配失败(即`malloc`函数返回`NULL`),*/
exit(OVERFLOW); /*则调用`exit`函数退出程序。*/
(*T)->data=ch; /*生成根结点*/
CreateBiTree(&(*T)->lchild); /*构造左子树*/
CreateBiTree(&(*T)->rchild); /*构造右子树*/
}
}
6.3.8 线索二叉树
-
利用空指针存储前驱后继地址
-
增加ltag和rtag 2个标识域,用于区分孩子和前驱后继
标识为0指向孩子,为1指向前驱后继 -
线索化成就是在遍历的过程中修改空指针的过程,代码如下:
BiThTree pre; /*全局变量,始终指向刚刚访问过的结点*/
/*中序遍历进行中序线索化*/
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); /*递归左子树线索化*/
if(!p->lchild) /*没有左孩子*/
{
p->LTag=Thread; /*前驱线索,Thread表示该结点有左孩子线索,也就是定义为1*/
P->lchild=pre; /*左孩子指针指向前驱*/
}
if(!pre->rchild) /*前驱没有右孩子*/
{
pre->RTag = Thread; /*后续线索*/
pre->rchild = p; /*前驱右孩子指针指向后继(当前结点p)*/
}
pre=p; /*保持pre指向p的前驱*/
InThreading(p->rchild); /*递归右子树线索化*/
}
}
- 线索二叉树遍历图示、代码如下:
- link为0,thread为1
/*T为头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的*/
/*最后一个结点。中序遍历二叉线索链表表示的二叉树T*/
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p; /*定义名为 `p` 的 `BiThrTree` 类型的指针*/
p = T->lchild; /*p指向根结点*/
while (p!=T) /*空树或遍历结束时,p==T*/
{
while (P->LTag==Link) /*当LTag==0时循环到中序序列第一个结点*/
p=p->lchild; /*执行循环A→B→D→H,到达H后,H->LTag!=link(即不等于0),退出循环*/
printf("%c",p->data); /*显示H结点数据,可以更改为其他对节点操作*/
while(p->RTag==Thread && p->rchild!=T)/*RTag==Thread就是等于1的意思*/
{
p=p->rchild;
printf("%c",p->data);
}
p=p->rchild; /*p进至其右子树根*/
}
return OK;
}
注意:代码中出现2次p=p->lchild,因tag值不同而含义不同,第一次为后继,第二次为右孩子。
6.4 树、森林与二叉树转换
-
树转二叉树,步骤如下:
- 所有兄弟之间加线;
- 仅保留双亲与长子之间的连线,删除与其他孩子的连线;
- 调整外形,使二叉树更具辨识度。
-
森林转二叉树,步骤如下:
- 将每棵树转为二叉树;
- 首棵树不动,其它树依次将根结点接为首棵树根结点的右孩子。
-
二叉树转树,步骤为树转二叉树的逆过程。
-
二叉树转森林,步骤为森林转二叉树的逆过程。
-
树与森林的遍历
- 树的先跟遍历、森林的前序遍历对应二叉树的前序遍历;
- 树的后跟遍历、森林的后序遍历对应二叉树的中序遍历。
第2条理解有些难度,很容易跟二叉树的后序遍历对应。为什么是跟中序对应,原因是二叉树到树、森林的形变导致。
6.5 赫夫曼树及其应用
-
赫夫曼树,带权路径长度WPL最小的二叉树,即最优二叉树。
-
如何搭建赫夫曼树:
- 把有权值的叶子结点按权值从小到大排序;
- 取出其中权值最小的2个叶子结点组成一棵新的二叉树结点为N1,权值为2叶子结点之和,将N1放入排序池中重新排序;
- 再从排序池中取出其中最小的两个节点组建新的二叉树,以此类推直至结束。
注意:从排序池中取出的结点就不再放回。
-
赫夫曼树重要应用:赫夫曼编码
- 构建好赫夫曼树后,将路径上权值按左右分别替换为0、1,从根结点至叶子结点所过路径组成的二进制码,即为赫夫曼编码。
- 赫夫曼编码长短不一,为避免编码混淆,则必须是任一字符的编码都不是另一个字符的编码的前缀,即前缀编码。
- 赫夫曼编码、解码过程均需相同的赫夫曼树,即收发双方必须约定好同样的赫夫曼编码规则。