树和二叉树
本章内容多以选择题或综合题的形式考查,但统考也会出涉及树遍历相关的算法题。
树和二叉树的性质、遍历操作、转换、存储结构和操作特性等,满二叉树、完全二叉树、线索二叉树哈夫曼树的定义和性质,都是选择题必然会涉及的内容。
遍历是二叉树的各种操作的基础,统考时会考查遍历过程中对结点的各种其他操作,而且容易结合递归算法和利用栈或队列的非递归算法。
读者需重点掌握各种遍历方法的代码书写,并学会在遍历的基础上,进行一些其他的相关操作。
其中递归算法短小精悍,出现的概率较大,请读者不要掉以轻心,要做到对几种遍历方式的程序模板烂熟千心,并结合定数脉的习题,才可以在考试中快速地写出漂亮的代码。
-
树的定义
-
树是n个结点的有限集,当n=0时,称为空树。
-
非空树有且仅有一个根节点。n>1时,其余结点可分为互不相关的有限集(根的子树)。显然树的定义是递归的。
-
树是一种递归的数据结构,树作为一种逻辑结构同时也是一种分层的结构
-
结点A的祖先是从根结点到A的唯一路径上除A以外的其他结点。
-
树的度和结点的度:结点的孩子个数是结点的度,节点度的最大值是树的度。
-
结点的深度(层次) 是从根开始自顶向下累加;结点的高度是从叶结点自底向上累加。
-
树的特点有:根节点无前驱,叶子节点无后继。其余结点有且只有1个前驱;树中所有结点有<=m个后继。
-
分支结点和叶结点:度大于0的结点称为分支结点(也称非终端结点);度为0(没有孩子结点)的结点称为叶结点(也称终端结点)。在分支结点中,每个结点的分支数就是该结点的度。
-
由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。
-
有序树和无序树:树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。
-
路径和路径长度:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
-
树的路径长度:从根节点到每个结点的路径长度的总和
-
森林:m(m>=0)棵互不相交的集合
-
-
树的性质
- 树的结点数n = = = 所有结点的度数之和 + + + 1。
- 度为 m m m的树中第i层上至多有 m i − 1 m^{i-1} mi−1个结点(i>1)。度为 m m m前 i i i层最多有 m i − 1 m − 1 \frac{m^{i}-1}{m-1} m−1mi−1个结点。
- 度为 m m m、具有 n n n个结点的树的最小高度h为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1)\rceil ⌈logm(n(m−1)+1)⌉。
- 度为 m m m、具有 n n n个结点的树的最大高度 h h h为 ( n − m ) + 1 (n-m)+1 (n−m)+1。
-
二叉树的定义和性质
二叉树是有序树。
-
满二叉树
- 每一层都是满的,所以有:1)叶子节点都在最后一层 2)没有度为1的结点 3)结点i的左孩子是2i,右孩子是2i+1,父节点是 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋。
-
完全二叉树
- 就是满从最后的结点 开始去掉结点。 1)只有最后两层有叶子节点 2)度为1的结点个数:1或0 3)叶子节点和分支结点的分界线是n/2 4)左孩子2i,右孩子2i+1。
-
完全二叉树的性质
-
n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1 n 1 = 0 或 1 n_1=0或1 n1=0或1 n = 2 n 2 + 1 + n 1 n=2n_2+1+n_1 n=2n2+1+n1
-
结点i所在的深度是 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n\rfloor+1 ⌊log2n⌋+1
-
-
二叉排序树
- 结点的值:左<根<右
-
平衡二叉树
- ∣ 左子树深度 − 右子树深度 ∣ |左子树深度-右子树深度| ∣左子树深度−右子树深度∣<=1
-
-
二叉树的存储结构
-
顺序存储 按照顺序放入数组中
- 左孩子2i,右孩子2i+1,i<=
⌊
n
2
⌋
\lfloor \frac{n}{2}\rfloor
⌊2n⌋则是分支结点,否则是叶子节点。
- 缺点:只是适合存储完全二叉树。碰到高h结点个数h的单支树需要 2 h − 1 2^h-1 2h−1个存储空间
- 左孩子2i,右孩子2i+1,i<=
⌊
n
2
⌋
\lfloor \frac{n}{2}\rfloor
⌊2n⌋则是分支结点,否则是叶子节点。
-
链式存储
-
结点需要:数据域(data)、左指针域(lchild)、右指针域(rchild)。
typedef struct BiTNode{ ElemType data; struct BiNode *lchild,*rchild; }BiTNode,*BiTree
-
n个结点,则度为n-1,那么需要用到n-1个指针,而一共有2n个指针,有n+1个空指针。可以用来构造线索二叉树。
-
树的链式存储,在删除所有结点时,要先删除孩子节点,从下向上,采用后序遍历。
-
-
-
二叉树的遍历
- 先中后
//根 左 右 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); } }
-
层次遍历(根 左 右)
void LevelOrder(BiTree T){ InitQueue(Q); //初始化辅助队列 BiTree p; EnQueue(Q,T); //根节点入队 while(!IsEmpty(Q)){ //队列不空就一直遍历 DeQueue(Q,p); //队头出队 并且访问 根 左 右 visit(p); if(p->lchild!=NULL) EnQueue(Q,p->lchild); if(p->rchild!=NULL) EnQueue(Q,p->rchild); } }
给出中序遍历序列和其他三种任意一个才能确定一个树。
-
线索二叉树
-
算法思想
传统二叉链表只能体现一种父子关系,利用空余的n+1个空指针指向结点的前驱或者后继,这样就可以像遍历单链表一样方便地遍历二叉树,加快了查找结点前驱和后继的速度。
可以给存储结构加两个tag 来标识指针存的是什么 -
代码实现
typedef struct ThreadNode{ ElemType data; struct BiNode *lchild,*rchild; int ltag,rtag; }ThreadNode,*ThreadTree //以中序遍历为例 线索化 二叉树的递归算法(无Head) void InThread(ThreadTree T){ if(T!=NULL){ InThread(T->lchild); visit(T); InThread(T->rchild); } } void visit(ThreadNode *q) { //pre指向前一个结点,q指向的是当前结点 if(q->lchild==NULL){ //左 q->lchild=pre; p->ltag=1; } if(pre!=NULL&&pre->rchild==NULL){//右 pre->rchild=q; pre->rtag=1; } pre=q; //向下继续 } void CreatInThread(ThreadTree T){ pre=NULL; if(T!=NULL){ InThread(T); if(pre->rchild==NULL) //处理最后一个结点的右指针 pre->rtag=1; } } //中序二叉树的遍历 //中序线索树查找第一个结点 ThreadNode *Firstnode(ThreadNode *p){//传根节点 while(p->ltag==0) p=p->lchilad; //中序序列第一个结点是左子树中第一个没有左孩子的结点(不一定是叶子结点) return p; } //中序线索树查找结点p的后继 ThreadNode *Nextnode(ThreadNode *p){ if(p->rtag==0) return Firstnode(p->rchild); //中序序列结点的后继是右子树中左边第一个没有左孩子的结点(不一定是叶子结点) else return p->rchild; } //中序线索树查找最后一个结点 ThreadNode *Lastnode(ThreadNode *p){ while(p->rtag==0) p=p->rchilad; return p; } //中序线索树查找结点p的前驱 ThreadNode *Prenode(ThreadNode *p){ if(p->ltag==0) return Lastnode(p->lchild); else return p->lchild; } signed main() { ThreadTree T; ThreadNode *pre; //全局变量 CreatInThread(T); return 0; }
-
-
树、森林
-
树的存储结构
-
双亲表示法
//只能找到双亲(除了根结点) #define Max_Size 100 typedef struct{ ElemType data; int parent; }PTNode; typedef struct{ PTNode nodes[Max_Size]; int n; }PTree;
-
孩子表示法
存储结构是单链表+顺序表,数组每个元素都是一个结点,每个元素都有一个单链表从左向右指向自己孩子节点。 -
孩子兄弟表示法(二叉树表示法)
算法思想:二叉链表作为存储结构,每个节点包裹三部分:结点值、左指针指向第一个孩子、右指针指向兄弟结点
-
-
树、森林和二叉树的转换
- 树转换为二叉树的规则:每个结点的左指针指向它的第一个孩子,右指针指向它在树中的相 邻右兄弟,这个规则也称左孩子右兄弟。根结点没有兄弟,因此树转换得到的二叉树没有右子树。
- 1)在兄弟结点之间加一连线;
- 2)对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;
- 3)以树根为轴心,顺时针旋转45°。
- 森林转换为二叉树:将森林转换为二叉树的规则与树类似。先将森林中的每棵树转换为二叉树,由任意一棵树对应的二叉树的右子树必空,森林中各棵树的根也可视为兄弟关系,将第二棵树对应的二叉树当作第一棵二叉树根的右子树……以此类推,就可以将森林转换为二叉树。
- 将森林中的每棵树转换成相应的二叉树;
- 每棵树的根也可视为兄弟关系, 在每棵树的根之间加一根连线;
- 以第一棵树的根为轴心顺时针旋转45°。
- 树转换为二叉树的规则:每个结点的左指针指向它的第一个孩子,右指针指向它在树中的相 邻右兄弟,这个规则也称左孩子右兄弟。根结点没有兄弟,因此树转换得到的二叉树没有右子树。
-
树和森林的遍历
就是先访问根结点和先访问子结点两种。
-
-
树和二叉树的应用
- 哈夫曼树
- 树中的结点常常被赋予一个表示某种意义的数值,称为该结点的权。
- 从树的根刀一个结点的路径长度 与 该结点上权值的乘积,称为该结点的 带权路径长度。
- 树中所有叶子结点的带权路径长度之和 称为该树的带权路径长度,记 W P L = ∑ i = 1 n w i l i WPL=\sum\limits_{i=1}^{n}w_il_i WPL=i=1∑nwili
- 哈夫曼树是在含n个带权叶结点的二叉树中,WPL最小的二叉树,也称最优二叉树。
- 构造哈夫曼树的过程共新建了n-1个结点,因此哈夫曼树的结点总数为2n-1。
- 哈夫曼编码
- 可变长度编码:允许对不同字符用不等长的二进制位表示。
- 固定长度编码:每个字符用等长的二进制位表示。
- 若没有一个编码是另一个编码的前缀,则称这样的编码是前缀编码。
- 这里放个其他博主的详解。
- 哈夫曼树
-
并查集
优化版#define SIZE 100 int UFSets[SIZE]; void Initial(int s[]){ for(int i=0;i<SIZE;i++) s[i]=-1; } //深度不超过⌊log2 n⌋+1 void Union(int s[],int Root1,int Root2){ if(Root1==Root2) return; if(s[Root2]>s[Root1]){ //root2结点更少 s[Root1]+=s[Root2]; //累加结点数 s[Root2]=Root1; //小树合并到大树 } else{ s[Root2]+=s[Root1]; //累加结点总数 s[Root1]=Root2; //小树合并到大树 } } 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; //返回根结点编号 }