一、树的基本概念
1.树的定义
树:树是n个结点的有限集。
空树:当n等于0,称为空树。
非空树:
①有且仅有一个特定的称为根的结点。
②当n>1时,其余结点可分为m个互不相交的有限集,其中每个集合本身又是一颗树,并且称为根的子树。
note:树的定义是递归的,即在树的定义中又用到了其自身。
树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下特点:①树的根节点没有前驱
②树中的所有结点都可以有零个或多个后继。
③n个结点的树中又n-1条边。
2.基本术语
- 祖孙、子孙、孩子、兄弟、堂兄弟
- 结点的度和树的度
树中一个结点的孩子个数称为结点的度
树中结点的最大度数称为树的度
note:
这里有一个理解错误的点:孩子一定是该结点的下一级。孩子不是孙子那些。
- 分支结点和叶结点
- 结点的深度、高度和层次
- 有序树和无序树
- 路径和路径长度
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的。
路径长度:路径上所经过的边的个数。
- 森林
m棵互不相交的树的集合。
3.树的性质
结点和度数的关系的应用:
①树的结点树n等于所有结点的度数之和加1。
结点的度数:代表了这个结点有几个孩子,就有几个结点。再加一个表示,把根节点也加上了。
②度为m的树中第i层至多有 个结点(i>1)结合等比数列来理解
③高度为h的m叉树至多有
结合等比数列求和来理解
结点和高度之间的分析:④最小高度:我觉得最小高度就穷举吧。(这样比较快)
⑤最大高度
一层一个孩子
二、二叉树的概念
1.二叉树的定义及其主要特性 ★
定义:二叉树种每个结点至多有两棵子树,并且子树有左右之分,其次序不能任意颠倒。
几个特殊的二叉树
满二叉树:树中的每层都含有最多的结点。并且除了叶结点之外每个结点的度数均为2。
完全二叉树:与满叉树作对比,当且仅当其每个结点都与高速为h的满二叉树中编号为1~n的结点一一对应时,成为完全二叉树。
二叉排序树:左子树中的值小于右子树对应的值。
平衡二叉树:树上任意结点的左子树和右子树的深度之差不超过1
性质
①非空二叉树上的叶结点数等于度为2的结点数加1,即
证明:注意:这里的、、代表的是度为0、1、2的结点
从总的n个结点来说:
从总的分支n而言:(度为1的提供1个,度为2的提供2个,外加一个根节点上面的分支)
②非空二叉树上第k层至多有个结点、高度为h的二叉树至多有个结点。
③对完全二叉树按从上至下、从左至右的顺序依次编号1,2,....,n,则有以下关系
note:我觉得如果出此类型的题,直接穷举就行了吧。
④具有 n个结点的完全二叉树的高度为 或note:我觉得如果出此类型的题,也直接穷举吧。
2.二叉树的存储结构
①顺序存储结构
定义:利用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点。就是将完全二叉树上编号为i的结点元素存储在一维数组下标为 i-1的分量中。
主要思想:就是将一颗普通的二叉树想象成完全二叉树的样子,依次按编号去存储元素,假如没有该编号对应没有结点,就将该位置上的数组值置为0。note:使用数组时,可以把让数组0不使用,然后性质3(二叉树的性质)就对应上了。
②链式存储结构
顺序存储来说,空间利用率较低,因此二叉树一般都采用链式存储结构。
于是,上面的存储结构就可以如图所示。
typedef struct BiTNode{ ElemType data; struct BiTNode *lchild,*rchild; //左、右孩子指针 }BiTNode,*BiTree;
三、二叉树的遍历和线索二叉树
1.二叉树的遍历
遍历:按某条搜索路径访问树中每个结点,使得每个结点均被有且仅被访问一次。
常见的遍历序列有 先序遍历,中序遍历,后序遍历。其中“序”指的是根结点何时被访问。
note:1.在递归遍历中,递归工作栈的深度为树的深度,所以在最坏情况下,二叉树是有n个结点且深度为n的单支树,遍历算法的空间复杂度为O(n)
2.需要将以下三种方式当做模板进行记忆,考研题大多是基于这三个模板延伸出来的。
①先序遍历
算法思想:
若二叉树为空,退出;
不为空,
访问根结点;
先序遍历左子树;
先序遍历右子树;
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); //访问根结点 } }
④中序遍历的非递归写法(!!!)
在递归算法中,有个 visit()函数,这个函数暂时不要理会。
eg:此二叉树的中序遍历结果 DBEAC
引入栈,来说明中序遍历的访问过程(思想),沿着根的左孩子,以此入栈,直到左孩子为空,说明可以找到输出的结点,此时栈内元素依次为A、B、D。
栈顶元素出栈并访问:若其右孩子为空,继续执行;
若其右孩子不空,将右子树转执行;
上述中序遍历的非递归过程D是中序遍历的第一个结点;若D的右孩子为空,栈顶B出栈并访问,B右孩子不空,将其右孩子E入栈。E左孩子为空,栈顶E出栈并访问;E右孩子为空,栈顶A出栈并访问;A右孩子不空,将其右孩子C入栈,C左孩子为空,栈顶C出栈并访问。
void inOrder2(BiTree T){ InitStack(S); BiTree p=T; //初始化栈S;P是遍历指针 while(P || !IsEmpty(S)){ if(P){ Push(S,p); //当前结点入栈 p=p->lchild; //左孩子不空,一直向左走 }else{ Pop(S,p); //栈顶元素出栈 visti(p); //访问出栈结点 p=p->rchild; //向右子树走,p赋值为当前结点的右孩子 } } }
后序遍历的非递归实现是三种遍历方法中最难的。因为在后序遍历中,要保证左孩子何右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程控制带来了难题。
⑤层次遍历(!!!)
算法思想:
层次遍历时,需要借助一个队列。首先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;
若它有右子树,则将右子树根结点入队;
---> 该结点的孩子结点都入队后,这个结点可以出队了。
......如此反复,直至队列为空。
代码案例:待做完大题时回来补充。
⑥由遍历序列构造二叉树
由二叉树的先序序列和中序序列、中序序列和后序序列可以唯一的确定一颗二叉树。
若只知道二叉树的先序序列和后序序列,则无法唯一确定一颗二叉树。
2.线索二叉树
1. 线索二叉树的基本概念
传统的二叉链表仅能体现一种父子关系,但是不能直接得到结点在遍历的前驱和后继。
在n个结点的二叉树中,总是存在n+1个空指针。
证明:叶子结点都有2个空指针,每个度为1的结点都有1个空指针--->现在空指针的个数都为。但是又因为,所以原式就等于.
我们可以利用这些空指针去存放指向前驱和后继的指针。(要是没有左孩子,空指针就指向前驱;要是没有右孩子,空指针就指向后继)
typedef struct ThreadNode{ ElemType data; //数据元素 struct ThreadNode *lchild,*rchild; //左右孩子指针 int ltag,rtag; //左右线索标志 }ThreadNode,*ThreadTree;
线索链表:以这种结点结构构成的二叉链表作为二叉树的存储结构。
线索:指向结点前驱和后继的指针。
线索二叉树:加上线索的二叉树
2. 中序线索二叉树的构造
二叉树的线索化:将二叉链表中的空指针指向前驱或后继的线索。
但是在线索化的时候,自己也不知道二叉树中有哪些指针是空的,因此在线索化的时候就需要自己去遍历一遍二叉树。
线索化思想:需要额外加一个指针pre指向刚刚访问过的结点,现有一个指针p指向正在访问的结点。(即pre指向p的前驱)。在中序遍历的时候,检查p的左指针是否为空,若为空就将它指向pre,检查pre的右指针是否为空,为空就将它指向p。
中序遍历对二叉树线索化的递归算法如下:
void InThrea(ThreadTree &p,ThreadTree &pre){ if(p!=NULL){ InThread(p->lchild,pre); //递归,线索化二叉树 if(p->lchild==NULL){ //当前结点的左子树为空 p->lchild=pre; //建立当前结点的前驱线索 p->ltag=1; } } if(pre!=NULL && pre->rchild==NULL){ //前驱结点非空且其右子树为空 pre->rchild=p; //建立前驱结点的后继线索 pre->rtag=1; } pre=p; //标记当前结点成为刚刚访问过的结点 InThread(p->rchild,pre); //递归,线索化右子树 }
3.中序线索二叉树的遍历
代码先放
手算:会计算二叉树中对应的前驱和后继。
4.先序线索二叉树和后序线索二叉树
四、树、森林
1.树的存储结构
①双亲表示法
采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。特殊的是,根结点的伪指针域为-1。
//代码省略
特点:
找双亲容易,找孩子难(求结点的孩子时,需要遍历整个结构)。
note:树的顺序存储结构 vs 二叉树的顺序存储结构
在树的顺序存储结构中,数组下标代表结点的编号,下标中所存的内容指示了结点之间的关系。
在二叉树的顺序结构中,数组下标既代表了结点的编号,也指示了二叉树中各结点之间的关系。
二叉树属于树,因此二叉树也可用树的存储结构来存储,但树却不能用二叉树的存储结构来存储。
②孩子表示法
用单链表作为存储结构。单链表中的头结点代表的是树中所有结点,头结点指向孩子结点的编号。
特点:找孩子容易,找双亲难(需要遍历n个头结点指向的链表结点)。
③孩子兄弟表示法
又称 二叉树表示法
每个结点包括三个部分内容:结点值、指向结点第一个孩子的指针,以及指向结点下一个兄弟结点的指针(沿着该结点的右指针,可以找到所有兄弟结点)。
特点:方便实现树和二叉树之间的转换,易于查找结点的孩子。
从当前结点查找双亲结点比较麻烦。
2.树、森林与二叉树的转换
①树转换二叉树
结点的左指针指向它的第一个孩子,右指针指向它在树中相邻兄弟。
②森林转换二叉树
先将森林中每颗树转换为二叉树,在各个二叉树的根结点连起来,再跟着感觉旋转一下,就成了。
③二叉树转换森林
先将根结点的右链断开,形成多棵二叉树。
再将多棵二叉树中各个结点的右链全部断开,连上属于他的双亲。
3.树和森林的遍历
①树的遍历
1)先序遍历
先访问根结点,再依次遍历根结点的每棵子树(遍历子树时仍遵循先根后树的规则)。
2)后序遍历
eg:先根遍历:ABEFCDG
后根遍历:EFBCGDA
中根遍历:EBFCDGA
②森林的遍历
1)先序遍历
2)中序遍历
note:三个孩子时,把孩子遍历完再遍历根结点。(所以中序遍历又称后序遍历)
eg:
先序遍历:ABCDEFGHI
后序遍历:BCDAFEHIG
五、树与二叉树的应用
1.哈夫曼树和哈夫曼编码
①哈夫曼树的定义
先来说几个概念
路径:从树中一个结点到另一个结点之间的分支。
路径长度:路径上的分支数目。
权:树中结点常常被赋予一个表示某种意义的数值。
带权路径长度:从树的根到一个结点长度与该结点上权值的乘积。
树的带权路径长度:树中所有叶结点的带权路径长度之和。
哈夫曼树:在含有n个带权叶结点的二叉树中,带权路径长度最小的二叉树。
②哈夫曼树的构造
先给n个权值不同得结点进行排序
接着从这里选择两个最小的结点,构成最低的叶子,
然后在剩下的结点中,挑最小的两个,看看加起来有没有刚才构成的结点的权值大
如果有,就用一个最小的和刚才的结点结合
如果没有,就让这俩结点结合形成一个新的结点
③哈夫曼编码
固定长度编码:对每个字符用相等长度的二进制位。
可变长度编码:允许对不同字符用不等长的二进制位表示。
前缀编码:在所有的编码中,没有一个编码是另一个编码的前缀。
设计哈夫曼编码:eg:左分支为0,右分支为1
2.并查集
2022年新考点
并查集:一种特殊的集合。
主要完成以下两种功能
1.“并”。将两个集合并起来
2.“查”。判断一个元素属于哪个集合。
1.并查集的存储结构
采用树的双亲表示法。
即集合中的孩子结点指向他的双亲结点在存储单元中的地址,根结点中地址值用-1表示。
“并查集” 的代码实现--初始化# define size 13 int UFSets[size ]; //集合元素数组 //初始化并查集 void Initial(int S[]){ for(int i=0;i<size;i++) S[i]=-1; }
2.并查集的基本操作
查:找x所属集合(返回x所属根结点)
int find(int S[],int x){ while(S[x]>=0) //循环寻找x的根 x=S[x]; return x; //根的s[]小于0 }
并:将两个集合合并为1个void Union(int S[ ],int root1,int root2){ //要求root1与root2是不同集合 if(root1==root2) retrun; //将根root2连接到另一根root1下面 S[root2]=root1 }
时间复杂度分析:
查的时间复杂度为O(n)。(当这课树瘦高瘦高的)
并的时间复杂度为O(1)。(只需要将树根结点对应的数组值改成另一棵树的树根在存储单元中的值)
3.并查集的优化
优化角度一:“并”操作时,让小树合并到大树上,让树高不再增长,这样可以减少查询的时间。
--->Q:如何区分大树和小树?
用根结点的绝对值表示树的结点总数。
// 小树合并到大树 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; //返回根结点编号 }