文章目录
基本概念
- 层次——深度;从上往下(一般从1开始)
- 高度:从下往上
- 有序树:子树从左到右有顺序,否则就是无序树
性质
- 节点数=总度数+1
- 树的度:各节点度的最大值
- 度为m的树第i层最多有 m i − 1 m^{i-1} mi−1个节点
- 高度为h的m叉树最多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m−1mh−1个节点
- 高度为h的m叉树至少有 h 个结点。高度为h、度为m的树至少有 h+m-1 个结点。
- 其有n个结点的m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_{m}(n(m - 1) + 1)\rceil ⌈logm(n(m−1)+1)⌉
二叉树
-
二叉树是有序树
-
二叉树性质
- 设非空二叉树中度为0、1和2的结点个数分别为 n 0 , n 1 , n 2 , n_{0},n_{1}, n_{2}, \quad n0,n1,n2, 则 n 0 = n 2 + 1 n_{0}=n_{2}+1 n0=n2+1 (叶子结点比二分支结点多一个)
- 二叉树第i层至多有 2 i − 1 2^{i-1} 2i−1 个结点( i ≥ 1 i\geq1 i≥1)
- 高度为h的二叉树至多有 2 h − 1 2^{h}-1 2h−1 个结点(满二叉树);高度为h的m叉树至多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m−1mh−1 个结点
满二叉树
- 高度为h的满二叉树,含有 2 h − 1 2^{h}-1 2h−1个节点
- 除了叶子节点其他节点都有两个子节点,不存在度为1的节点(要么为2要么为0)
- 按层序从1开始编号,则第i个节点的左孩子为2i,右孩子节点为2i+1;父节点为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
完全二叉树
按层序进行编号,每个节点对应的编号和相同高度的满二叉树对应的编号相同
- 只有最后两层可能有叶子节点
- 最多只有一个度为1的节点,且一定是编号最大的分支节点,对应的子节点一定是其左孩子节点
- 按层序从1开始编号,则第i个节点的左孩子为2i,右孩子节点为2i+1;父节点为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
- i ≤ ⌊ n / 2 ⌋ i\leq \lfloor n/2\rfloor i≤⌊n/2⌋为分支节点, i > ⌊ n / 2 ⌋ i> \lfloor n/2\rfloor i>⌊n/2⌋为叶子节点
- 具有 n (n>0) 结点的完全二叉树的高度h为 ⌈ log 2 ( n + 1 ) ⌉ \left\lceil\log _{2}(n+1)\right\rceil ⌈log2(n+1)⌉或 ⌊ l o g 2 n ⌋ + 1 \left\lfloor l o g_{2} n\right\rfloor+1 ⌊log2n⌋+1
- 对于完全二叉树,可以由节点数目推出度为0,1,2 的节点数目:度为1的节点数目只能是0或者1,又 n 0 = n 2 + 1 n_{0}=n_{2}+1 n0=n2+1,所以度为0,2的节点数目之和为奇数,因此总数为偶数时,度为1的节点数为1,否则为0,从而推出剩下两种节点的数目。
二叉排序树(BST )
常用于元素的排序和搜索,又称二叉查找树
二叉排序树是一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均
小于(没有等于)
根结点的关键字: - 右子树上所有结点的关键字均
大于(没有等于)
根结点的关键字。 - 左子树和右子树又各是一棵二叉排序树。
中序遍历结果递增
所有节点的值都不同
应用
- 查找:递归和非递归方法
- 插入:和查找方法类似
- 构造二叉排序树( 结果与构造顺序有关)
- 删除节点:对于子树数目0(直接删除),1(删除后用子树替换),2(用中序序列对应的直接后继替换)分别处理。
删除后再插入结果可能和原始树不同
- 查找效率,
ASL
(平均查找长度),查找长度
——查找经过的节点数目,取决于树的高度,尽量把树变成平衡二叉树的形式(效率最高)
平衡二叉树
树上任一结点的左子树和右子树深度之差不超过1。
具有更高的搜索效率
应用
-
高度为h的最小平衡二叉树的节点数为:
- 高度为0的最小平衡二叉树节点数为 N 0 = 0 N_{0}=0 N0=0
- 高度为1的最小平衡二叉树节点数为 N 1 = 1 N_{1}=1 N1=1
- 高度为h的最小平衡二叉树节点数的递推公式为: N h = N h − 1 + N h − 2 + 1 N_{h}=N_{h-1}+N_{h-2}+1 Nh=Nh−1+Nh−2+1
-
平衡二叉树的判断
void Judge_AVL(BiTree bt, int &balance, int &h){
int bl=0, br=0, hl=0, hr=0;
if(bt==NULL){
h=0;
balance=1;
}
else if (bt->lchild==NULL &&bt->rchild==NULL){
h=1;
balance=1;
}
else{
Judge_AVL(bt->lchild, bl, hl);
Judge_AVL(bt->rchild, br, hr);
if(hl>hr)
h=hl+1;
else
h=hr+1;
if(abs(hl-hr)<2 && bl==1 && br==1)
balance=1;
else
balance=0;
}
-
插入:先插入后调整,每次调整最小不平衡子树
-
LL平衡旋转(右单旋转):原因是在A左孩子的左子树上插入了节点。将A的左孩子B替代A,将节点A成为B的右子树的根节点,B的原右子树变成A的左子树
-
RR平衡旋转(左单旋转):原因是在A右孩子的右子树上插入了节点。将A的右孩子B替代A,将节点A成为B的左子树的根节点,B的原左子树变成A的右子树
-
LR平衡旋转(先左后右双旋转):原因是在节点A的左孩子的右子树上插入了新的节点。将A的左孩子B的右孩子节点C替换B,C的左孩子称为B的右孩子节点,然后用节点C向上替换节点A的位置。
-
RL平衡旋转(先右后左双旋转):原因是在节点A的右孩子的左子树上插入了新的节点。将A的右孩子B的左孩子节点C替换B,C的右孩子称为B的左孩子节点,然后用节点C向上替换节点A的位置。
-
线索二叉树
定义及特性
- 若无左子树,则将节点左指针指向前驱节点
- 若无右子树,则将节点右指针指向后继节点
前驱/后继节点是依据在遍历序列(前/中/后序遍历)中的顺序确定的
每个节点需要增加两个结点用于表示对应的指针是指向孩子节点还是指向前驱/后继
中序线索二叉树
- 基本特性
- 前驱结点
- 若左指针为线索,则其指向结点为前驱结点
- 若左指针为左孩子,则其左子树的最右侧结点为前驱结点
- 后驱结点
- 若右指针为线索,则其指向结点为后驱结点
- 若右指针为右孩子,则其右子树的最左侧结点为后驱结点
- 中序线索二叉树线索化
void InThread(ThreadTree &p, ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild, pre);
if(p->lchild==NULL){
p->lchild=pre;
pre->ltag=1;
}
if(pre!=NULL && pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
InThread(p->rchild, pre);
}
}
void CreateInThread(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
InThread(T, pre);
//完善最后一个节点。最后一个节点的后继为空
pre->rchild=NULL;
pre->rtag=1;
}
}
- 中序线索二叉树遍历
ThreadNode *Firstnode(threadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}
ThreadNode * Nextnode(ThreadNode *p){
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T); p!=NULL; p=Nextnode(p))
visit(p); //visit()为中序遍历函数
}
树的存储结构
- 双亲表示法:采用一组连续的存储空间来存储每个结点, 同时在每个节点中增设一个伪指针, 指示双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1。
- 孩子表示法:将每个结点的孩子结点都用单链表连接起来形成一个线性结构,n个结点具有n个孩子链表。
- 孩子兄弟表示法(左孩子右兄弟):以二叉链表作为树的存储结构,又称二叉树表示法。
n个节点的二叉链表,共有2n个指针域,其中n-1个非空(n-1个孩子节点),n+1个空指针
不同存储结构的比较
树、森林与二叉树的转化
转换
- 树与二叉树转换:孩子兄弟表示法
- 森林与二叉树转换:孩子兄弟表示法(将根节点视为兄弟节点)
遍历
树的遍历:
- 先根:先访问根节点,从左到右依次先根遍历各个子树;和对应的二叉树的先序遍历结果相同
- 后根:从左到右依次后根遍历各个子树,再访问根节点;和对应的二叉树的中序遍历结果相同
- 层次遍历
森林的遍历
- 先序遍历:对每棵树先根遍历所有树;和对应二叉树的先序遍历结果相同
- 中序遍历:后根遍历每棵树;和对应的二叉树的中序遍历结果相同。
遍历序列的对应关系
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
二叉树的遍历
- 层次遍历
算法思想:- 初始将根入队并访问根结点,然后出队;
- 若有左子树, 则将左子树的根入队;
- 若有右子树, 则将右子树的根入队:
- 然后出队,访问该结点;
- 反复该过程直到队列空为止。
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);
}
}
由遍历序列构造二叉树:中序遍历序列+前序/后序/层次
- 中序遍历非递归(使用栈)
算法思想:- 初始时依次扫描根结点的所有左侧结点并将它们一一进栈;
- 出栈一个结点,访问它;
- 扫描该结点的右孩子结点并将其进栈;
- 依次扫描右孩子结点的所有左侧结点并一一进栈;
- 反复该过程直到栈空为止。
void InOrder2(BiTree T){
InitStack(S);
BiTree p=T;
while(p||!isEmpty(S)){
if(p){
Push(S, p);
p=p->lchild;
}
else{
Pop(S, p);
visit(p);
p=p->rchild;
}
}
}
树的应用
哈夫曼树
- 基本概念
路径长度
:路径上经过的边的个数树的带权路径长度
:树中所有叶节点的权重乘路径的和- 哈夫曼树也称最优二叉树,含有n个带权叶子节点带权路径长度最小的二叉树。
- 哈夫曼树的构造
- 将n个结点作为n棵仅含有一个根结点的二叉树, 构成森林F
- 生成一个新结点,并从F中找出根结点权值最小的两棵树作为它的左右子树, 且新结点的权值为两棵子树根结点的权值之和;
- 从F中删除这两个树, 并将新生成的树加入到F中
- 重复2,3步聚, 直到F中只有一棵树为止。
- 哈夫曼树特点
- 每个初始结点都会成为叶节点,双支结点都为新生成的结点
- 权值越大离根结点越近,反之权值越小离根结点越远
- 哈夫曼树中没有结点的度为1
- n个叶子结点的哈夫曼树的结点总数为2n-1,其中度为2的结点数 为n-1。
- 哈夫曼树并不区分左右子树,因此哈夫曼树不唯一。
- 应用
编码问题;
前缀编码(没有任何一个编码是另一个编码的前缀)
并查集
一种简单的集合表示;常用树的双亲表示法作为并查集的存储结构;通常用数组元素的下标代表元素名, 用根结点的下标代表子集合名,根结点的双亲结点为负数。
内容来源:王道考研——数据结构