一、基本概念
- 空树:0个结点
- 子树:当n>1时,其余结点可分为m(m>0)个互不相交的有限集Ti,T₂,…,Tm,其中每个集合本身又是一棵树,并且称为根的子树
- 层次:根节点为第1层
- 深度:结点所在的层次
- 树的高度:树中结点的最大层数
- 结点的高度:以该节点为根的子树的高度
- 结点的度:结点的孩子个数
- 树的度:树中结点的最大度数
- 分支结点:度大于0。叶结点:度为0
- 有序树:结点的各个子树从左到右有次序,不能互换。否则为无序树
- 路径:两个结点之间经过的结点序列(从上到下。同一双亲的两个孩子之间不存在路径)
- 路径长度:路径上经过的边的个数
- 森林:m(m>=0)棵互不相交的树的集合
- m叉树:每个结点最多有m个孩子,允许所有结点的度都小于m,可以是空树
- 度为m的树:每个结点最多有m个孩子,至少有一个结点度=m,一定是非空树,至少有m+1个结点
二、树的性质
- 树的结点数n等于所有结点的度数之和加1。
- 度为m的树中第i层上至多有m^(i-1)个结点(i≥1)。
- 高度为h的m叉树至多有(m^h-1)/(m-1)个结点,至少有h个结点
- 度为m、具有n个结点的树的最小高度h为[logm(n(m-1)+1)]。
- 度为m、具有n个结点的树的最大高度h为n-m+1。
- 高度为h,度为m的树至少有h+m-1个结点
三、二叉树
(一)基本概念
- 二叉树是有序树
- 五种形态:空二叉树、只有根节点、只有左子树、只有右子树、左右子树都有
- 二叉树与度为2的有序树的区别: ①度为2的树至少有3个结点,而二叉树可以为空。 ②度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一结点而言的,而是确定的。
(二)特殊二叉树
- 满二叉树。
高度为h,有2^h-1个结点。即二叉树中的每层都含有最多的结点。满二叉树的叶结点都集中在二叉树的最下一层,并且除叶结点之外的每个结点度数均为2。
按层序编号:编号从根结点(根结点编号为1)起,自上而下,自左向右。对于编号为i的结点,若有双亲,则其双亲为⌊i/2⌋,若有左孩子,则左孩子为2i;若有右孩子,则右孩子为2i+1。 - 完全二叉树
高度为h,有n个结点,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树(完全二叉树可视为从满二叉树中删去若干最底层、最右边的一些连续叶结点后所得到的二叉树。) - 二叉排序树
左子树上所有结点的关键字均小于根结点的关键字;右子树上所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一棵二叉排序树。 - 平衡二叉树
树中任意一个结点的左子树和右子树的高度之差的绝对值不超过1 - 正则二叉树
树中每个分支结点都有2个孩子,即树中只有度为0或2的结点。
(三)性质
- 非空二叉树上的叶结点数等于度为2的结点数加1,即no=n₂+1
- 非空二叉树的第k层最多有2^(k-1)个结点(k≥1)。
- 高度为h的二叉树至多有2^h-1个结点(h≥1)。
- 对完全二叉树按从上到下、从左到右的顺序依次编号1,2,…,n,则有以下关系:
①最后一个分支结点的编号为⌊n/2⌋,若i≤⌊n/2⌋,则结点i为分支结点,否则为叶结点。 ②叶结点只可能在最后两层上出现(相当于在相同高度的满二叉树的最底层、最右边减少一些连续叶结点,当减少2个或以上叶结点时,次底层将出现叶结点)。 ③若有度为1的结点,则最多只可能有一个,且该结点只有左孩子而无右孩子(度为1的分支结点只可能是最后一个分支结点,其结点编号为⌊n/2⌋)。 ④按层序编号后,一旦出现某结点(如编号i)为叶结点或只有左孩子的情况,则编号大于i的结点均为叶结点(与结论①和结论③是相通的)。 ⑤若n为奇数,则每个分支结点都有左、右孩子;若n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子,没有右孩子,其余分支结点都有左、右孩子。 ⑥当i>1时,结点i的双亲结点的编号为⌊i/2⌋。 ⑦若结点i有左、右孩子,则左孩子编号为2i,右孩子编号为2i+1。 ⑧结点i所在层次(深度)为⌊log₂i⌋+1。 - 具有n个(n>0)结点的完全二叉树的高度为「log₂(n+1)⌉或⌊log₂n⌋+1
- 完全二叉树最多只有一个度为1的结点,即n1=0或1,no=n₂+1,若完全二叉树有2k个结点,则必有n1=1,n0=k,n2=k-1.若有2k-1个结点,则必有n1=0,n0=k,n2=k-1
(四)结构存储
1. 顺序存储
一维静态数组,建议从t[1]开始存,保证数组下标和结点编号一致
如果不是完全二叉树和满二叉树,则会有一些空结点value=0,浪费空间
#define MaxSize 100
struct TreeNode {
ElemType value;//结点中的数据元素
bool isEmpty;//结点是否为空
};
TreeNode t[MaxSize];
//初始化,所有结点标记为空
for(int i=0; i<MaxSize; i++) {
t[i].isEmpty=true;
}
//基本操作
i的左孩子:2i
i的右孩子:2i+1
i的父结点:i/2向下取整
i所在的层次:「log₂(n+1)⌉或⌊log₂n⌋+1
//完全二叉树中共有n个结点,以下情况的条件
i有左孩子:2i<=n
i有右孩子:2i+1<=n
i失败叶子结点:i>(n/2向下取整) 否则为分支结点
2. 链式存储
在含有n个结点的二叉链表中,含有n+1个空链域
typedef struct BiTNode {
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
} BiTNode, *BiTree;
BiTree root=NULL;//定义一棵空树
//插入根节点
root = (BiTree)malloc(sizeof(BiTNode))
;
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;//作为根节点的左孩子
(五)遍历
1.递归
时间复杂度均为O(n),递归栈的深度=树的深度
先序对应前缀表达式,中序对应中缀(需要加括号),后序对应后缀
//先序 中左右
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
//中序 左中右
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
//后序 左右中
void PostOrder(BiTree T) {
if (T != NULL) {
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
2.层次遍历
用队列。①根节点入队②若队列非空,则队头结点出队,访问该结点,若它有左孩子,则将其左孩子入队;若它有右孩子,则将其右孩子入队。③重复步骤②,直至队列为空。
void LevelOrder(BiTree T) {
InitQueue(Q); //初始化辅助队列 BiTree p;
EnQueue(Q,T); //将根结点入队
while (!IsEmpty(Q)) { //队列不空则循环
DeQueue(Q,p); //队头元素出队
visit(p); //访问当前p所指向结点
if (p->lchild!=NULL)
EnQueue(Q,p->lchild);//左子树不空,则入队列
if (p->rchild!=NULL)
EnQueue(Q,p->rchild);//右子树不空,则入队列
}
}
3.中序遍历的非递归算法,借助一个栈
void InOrder2(BiTree T) {
InitStack(S);
BiTree p = T; //初始化栈;p是遍历指针
while (p||!IsEmpty(S)) { //栈不空或p不空时循环
if (p) { //一路向西~不对一路向左(
Push(S,p); //当前节点入栈
p = p->lchild;//左子树不空便继续往左走
}
else { //退栈,访问根结点,遍历右子树
Pop(S,p); visit(p); //退栈,访问根结点
p = p->rchild; //再向右子树走
}
}
}
先序、中序、后序、层次,四种序列只给出一种,无法唯一确定一棵二叉树。若已知中序序列,再给出其他三种的任意一种,就可以唯一确定一棵二叉树
(五)线索二叉树
遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个和最后一个除外)都有一个直接前驱和直接后继。
找后继:若p->rtag=1则next=p->rchild;若p->rtag=0则为其右子树中最左下结点
找前驱:若若p->ltag=1则pre=p->lchild;若p->ltag=0则为其右子树中最右下结点
//若无左子树,令ltag=1,1child指向其前驱结点;若无右子树,令rtag=1,rchild指向其后继结点
typedef struct ThreadNode {
ElemType data; //数据元素
struct ThreadNode *lchild, *rchild; //左、右孩子指针
int ltag, rtag; //左、右线索标志
} ThreadNode, *ThreadTree;
1.中序线索二叉树
1.寻找中序前驱的土方法
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);//递归遍历左子树
visit(T);//访问根结点
InOrder(T->rchild);//递归遍历右子树
}
}
//访问结点q
void visit(BiTNode * q){
if (q==p)//当前访问结点刚好是结点p
final = pre;//找到p的前驱
else
pre = q;//pre指向当前访问的结点
//辅助全局变量,用于查找结点p的前驱
BiTNode *p;//p指向目标结点
BiTNode * pre=NULL;//指向当前访问结点的前驱
BiTNode * final=NULL;//用于记录最终结果
2.中序遍历对二叉树线索化的递归算法
ThreadNode *pre = NULL;//全局变量pre,指向当前访问节点的前驱
void CreateInThread(ThreadTree T) {
pre = NULL;
if(T!=NULL) { //非空二叉树才能线索化
InThread(T);
if(pre->rchild == NULL)//不写if直接设为null也行
pre->rtag = 1;
}
void InTread(ThreadTree T) {
if (T!=NULL) {
InTread(T->lchild);
visit(T);
InTread(T->rchild);
}
}
void visit(ThreadNode *p) {
if (p->lchild == NULL) { //左子树为空建立前驱线索
p->lchild = pre;
p->ltag = 1;
}
if (pre!=NULL && pre->rchild == NULL) {
pre->rchild = p; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
}
(1)求中序线索二叉树中序序列下的第一个结点
ThreadNode *Firstnode(ThreadTree p) {
while (p->ltag == 0)
p = p->lchild; //最左下结点(不一定是叶结点)
return p;
}
(2)求中序线索二叉树中结点p在中序序列下的后继结点
ThreadNode *Nextnode(ThreadNode *p) {
if (p->rtag == 0)//若p有右子树
return Firstnode(p->rchild);//返回右子树中最左下结点
else return p->rchild; //rtag==1直接返回后继线索
}
(3)求中序线索二叉树中序序列下的最后一个结点
ThreadNode *Lastnode(ThreadTree p) {
while (p->rtag == 0)
p = p->rchild;(最右下)
return p;
}
(4)求中序线索二叉树中结点p在中序序列下的前驱结点
ThreadNode *Prenode(ThreadNode *p) {
if (p->ltag == 0)
return Lastnode(p->lchild);
else
return p->lchild;
}
(5)不带头结点的中序线索二叉树的中序遍历
//无需递归,空间复杂度O(1)
void InOrder(ThreadTree T) {
for (ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
visit(p);
}
(6)不带头结点的中序线索二叉树的逆向中序遍历
void InOrder(ThreadTree T) {
for (ThreadNode *p = Lastnode(T); p != NULL; p = Prenode(p))
visit(p);
}
为方便起见,可在二叉树的线索链表上也添加一个头结点,令其lchild域的指针指向二叉树的根结点,其rchild域的指针指向中序遍历时访问的最后一个结点;令二叉树中序序列中的第一个结点的1child域指针和最后一个结点的rchild域指针均指向头结点。这好比为二叉树建立一个双向线索链表
2.先序线索化
存在转圈问题,需要修改
找后继:先序线索二叉树中,若有左孩子,其左孩子为后继,若无左孩子但有右孩子,则右孩子为后继;若为叶节点,则右链域直接指示了结点的后继
无法找前驱,除非从头遍历
ThreadNode *pre = NULL;//全局变量pre,指向当前访问节点的前驱
void CreatePreThread(ThreadTree T) {
pre = NULL;
if(T!=NULL) { //非空二叉树才能线索化
InThread(T);
if(pre->rchild == NULL)//不写if直接设为null也行
pre->rtag = 1;
}
void PreTread(ThreadTree T) {
if (T!=NULL) {
visit(T);
if(T->ltag == 0)//lchild不是前驱线索
PreThread(T->lchild);
InTread(T->lchild);
InTread(T->rchild);
}
}
void visit(ThreadNode *p) {
if (p->lchild == NULL) { //左子树为空建立前驱线索
p->lchild = pre;
p->ltag = 1;
}
if (pre!=NULL && pre->rchild == NULL) {
pre->rchild = p; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
}
3.后序线索化
找前继:若p有右孩子,则前继为右孩子;若p无右孩子,则前继为左孩子;
找后继:只能用遍历法
在后序线索二叉树中找结点,可分三种情况:①若结点x是二叉树的根,则其后继为空;②若结点x是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继即双亲;③若结点x是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树上按后序遍历列出的第一个结点。图5.19(c)中找结点B的后继无法通过链域找到,可见在后序线索二叉树上找后继时需知道结点双亲,即需采用带标志域的三叉链表作为存储结构。
ThreadNode *pre = NULL;//全局变量pre,指向当前访问节点的前驱
void CreatePreThread(ThreadTree T) {
pre = NULL;
if(T!=NULL) { //非空二叉树才能线索化
InThread(T);
if(pre->rchild == NULL)//不写if直接设为null也行
pre->rtag = 1;
}
void PreTread(ThreadTree T) {
if (T!=NULL) {
InTread(T->lchild);
InTread(T->rchild);
visit(T);
}
}
void visit(ThreadNode *p) {
if (p->lchild == NULL) { //左子树为空建立前驱线索
p->lchild = pre;
p->ltag = 1;
}
if (pre!=NULL && pre->rchild == NULL) {
pre->rchild = p; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
}
总结:中、先、后序二叉树中,先序不能找前驱(除非用三叉链表或者从头遍历),后序不能找后继(除非用三叉链表或者从头遍历),其余都可以。
四、树的存储结构
(一)双亲表示法
用一组连续空间,数组,每个结点中增设一个伪指针,指示其双亲结点的位置,根结点下标为0,其伪指针域为-1
求双亲:直接得出
求结点的孩子:需要遍历整个结构
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct { //树的结点定义
ElemType data; //数据元素
int parent; //双亲位置域
} PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE];//双亲表示
int n; //结点数
}PTree;
(二)孩子表示法
将每个结点的孩子结点视为一个线性表,用单链表,n个结点就有n个孩子链表(叶结点的孩子链表为空表)
求孩子:直接得出
求双亲:需要遍历n个结点中孩子链表指针域所指向的n个孩子链表
struct CTNode {
int child;//孩子结点在数组中的位置
struct CTNode *next;//下一个孩子
} ;
typedef struct {
ElemType data;
struct CTNode *firstChild;//第一个孩子
} CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n,r;//节点总数n,根的位置r=0
}CTree;
//没有孩子的结点,firstChild=NULL
(三)孩子兄弟表示法(二叉树表示法)
用二叉链表
灵活,可以方便地实现树转换二叉链表的操作
查孩子:方便;
查双亲:麻烦(若为每个结点增设一个parent域指向其父结点,则查父结点也方便了)
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firstchild,*nextsibling;//第一个孩子 和右兄弟指针
}CSNode,*CSTree;
五、树、森林与二叉树的转换
(一)树转换为二叉树
就是用孩子兄弟表示法存储树
- 在兄弟结点之间加连线
- 对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉
- 以树根为轴心,顺时针旋转 45°
(二)森林转换为二叉树
- 将森林中的每棵树转换成相应的二叉树
- 每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线
- 以第一棵树的根为轴心顺时针旋转 45°。
- 或者先在森林中每棵树的根之间加一根连线,然后再采用树转换为二叉树的方法
(三)二叉树转换为森林
若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,所以将根的右链断开。二叉树根的右子树又可视为一个由除第一棵树外的森林转换后的二叉树,应用同样的方法,直到最后只剩一棵没有右子树的二叉树为止,最后将每棵二叉树依次转换成树,就得到了森林。
二叉树转为树或森林是唯一的
六、树、森林的遍历
(一)树的遍历
1.先根遍历
先访问根节点,再访问根节点的各个子树
void PreOrder(TreeNode *R) {
if(R!=NULL){
visit(R);//访问根节点
while(R还有下一个子树)
PreOrder(T);
}
}
2.后根遍历
先访问根节点的各个子树,再访问根节点
void PreOrder(TreeNode *R) {
if(R!=NULL){
while(R还有下一个子树)
PreOrder(T);
}
visit(R);//访问根节点
}
3.层次遍历
用队列:①若树非空,则根节点入队②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队③重复②直到队列为空
(二)森林的遍历
1.先序遍历森林
- 访问森林中第一棵树的根结点。
- 先序遍历第一棵树中根结点的子树森林。
- 先序遍历除去第一棵树之后剩余的树构成的森林。
2.中序遍历森林。
- 中序遍历森林中第一棵树的根结点的子树森林。
- 访问第一棵树的根结点。
- 中序遍历除去第一棵树之后剩余的树构成的森林。
例子见书上p174重要
五、树与二叉树的应用
(一)基本概念
- 结点的路径长度:从根结点到该结点的路径上分支的数目
- 树的路径长度:树中每个结点的路径长度之和
- 结点的带权路径长度:从根结点到该结点的路径长度(lk)与结点上权(wk)的乘积。
- 树的带权路径长度:树中所有叶子结点的带权路径长度之和,WPL(T) = Σwk·lk (对所有叶子结点)。
(二)赫夫曼树
1.定义
在所有含 n 个叶子结点、并且叶子结点带相同权值的二叉树中,其带权路径长度WPL最小的那棵二叉树称为最优二叉树。
2.构造最优二叉树——赫夫曼算法
采用贪心策略。组成树的每个结点作为一棵树,从中选取权值为最小和次小的两个,分别作为左、右子树构造一棵新的二叉树,并置这棵新的二叉树根结点的权值为其左、右子树根结点的权值之和;从所有树中中删去这两棵树,同时加入刚生成的新树。重复选取根结点权值最小的两个,直至 F 中只含一棵树为止(重复 n-1次)。
- n个叶子结点的赫夫曼树共有2n-1个结点
- 赫夫曼树中没有度为1的结点,结点总数为n0+n2
- 哈夫曼树不唯一,但WPL必然相同且为最优
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
3.哈夫曼编码
- 前缀编码:任何一个字符的编码都不是另一个字符的编码的前缀。
- WPL=Σ(每个字符编码长度*lk出现频次wk)
- 哈夫曼编码不唯一
(三)并查集
1.概念
并查集是一种简单的集合表示,它支持以下3种操作:
- Initial(s):将集合s中的每个元素都初始化为只有一个单元素的子集合。
- Union(S,Root1,Root2):把集合s中的子集合 Root2 并入子集合 Root1。要求 Root1和 Root2 互不相交,否则不执行合并。
- Find(s,x):查找集合s中单元素x所在的子集合,并返回该子集合的根结点。
2.存储结构
用树的双亲表示作为并查集的存储结构,每个子集合以一棵树表示。所有表示子集合的树,构成表示全集合的森林,存放在双亲表示数组内。用数组元素的下标代表元素名,用根结点的下标代表子集合名,根结点的双亲域为负数(可设置为该子集合元素数量的相反数)。
集合元素的编号从0到SIZE-1。其中 SIZE 是最大元素的个数。
//结构定义
#define SIZE 100
int UFSets[SIZE]; //集合元素数组(双亲指针数组)
//初始化
void Initial(int S[]) {
for (int i = 0; i < MaxSize; i++) //每个自成单元素集合
S[i] = -1;
}
//Find查操作,找x所属集合,返回x所属根节点
//该集合有n个结点,最坏(n为树的深度)时间复杂度O(n)
int Find(int S[], int x) {
while (S[x] >= 0) //循环寻找x的根
x = S[x];
return x; //根的S[]小于0
}
//判断两个元素是否属于同一集合,只需分别找到它们的根,再比较根是否相同即可。
//Union并操作,将两个集合合并为一个
//时间复杂度O(1)
void Union(int S[], int Root1, int Root2) {//Root1与Root2不同并且表示子集合的名字
S[Root2] = Root1; //将根Root2连接到另一根Root1下面
}
//改进的Union:判别子集中的成员数量,然后令成员少的根指向成员多的根,即把小树合并到大树,为此可令根结点的绝对值保存集合树中的成员数量。这样尽量不让树变高
void Union(int S[],int Rootl,int Root2) {
if(Root1==Root2) return;
if(S[Root2]>S[Root1]){//Root2 结点数更少
S[Root1]+=S[Root2];//累加集合树的结点总数.根节点的绝对值表示树的结点总数
S[Root2]=Rootl;//小树合并到大树
else{
S[Root2]+=S [Root1];
S[Root1]=Root2;
}
}
//采用这种方法构造得到的集合树,其深度不超过⌊log(2,n)」+1。
//改进的find:让查找路径变短
//当所査元素x不在树的第二层时,在算法中增加一个压缩路径的功能,即将从根到元素x路径上的所有元素都变成根的孩子。
int Find(int S[], int x){
int root=x;//循环找到根
while(S[root]>=0)
root=S[root];//压缩路径
while(x!=root){
int t=S[x];//t 指向x的父结点
S[x]=root;//x直接挂到根结点下面
x=t;
}
return root;//返回根结点编号
}