第五章:树和二叉树🌲🌳
知识点:
- 树的概念
树的性质:-
-
-
考题:一些概念
-
树的路径长度是从树根到每个结点的路径长度的总和
-
P131,t6
-
P131,t10
-
‼️重要性质:n个结点的树具有n-1条边,那么对于每一棵树,其结点数比边数多1,结点树比边数多n,就有n棵树。
-
-
-
-
二叉树
-
概念
-
满二叉树与完全二叉树的异同
-
正则二叉树
-
-
-
性质
-
-
高度
-
考题
-
第i结点的左孩子,不一定为2i,因为未必存在
-
⚠️注意:树的性质难题,往往是给定一些条件,让你确定某些说法是否正确,这种题一般选项都难以判断,解决办法是从定义下手,回想该树的定义,然后从特殊的情况下手,比方说根节点,既满足原本定义,又符合题目要求,即可选出答案
-
P139,t18
-
-
性质的应用,综合题:P140,t4、t5、t6
-
-
-
遍历
-
与各表达式的异同
-
应用
-
注意⚠️:给定了一个遍历序列,无法推出具体的树
-
缘由
-
-
-
-
南邮的话需要了解,遍历的递归算法与非递归算法都要学会
-
遍历的考题:P155(t8、t9、t20、t21)
-
-
存储
-
注意二叉树的顺序存储结构,只适合完全二叉树 如果存储非完全二叉树,需要用isEmpty来判断是否有左右孩子
-
存储非完全二叉树的缺陷,太浪费空间
-
-
-
-
二叉树、树、森林
-
主要考点(转化为二叉树)
-
“左孩子,右兄弟“
-
重要性质
1、分支节点的最右孩子一定是叶子结点
2、森林转化的二叉树,最右边是叶子结点
3、左指针为空的没有孩子(叶节点)
-
-
-
哈夫曼树
-
主要考点
-
重要性质
-
结点为奇数2n-1(合并n-1次)
-
每个子树,对应左右子树都可以任意看成0/1,即固定一部分,看后续有没有违反
P198:t18
-
-
-
-
线索二叉树
-
中序线索二叉树
-
-
-
代码:⚠️都别忘了处理最后一个结点
-
中序
-
先序线索化,需要注意判断ltag==0
-
-
-
-
-
-
进阶:学会写所有遍历的算法,递归的非递归的,线索化代码,手推前中后序线索化,找前驱后继的过程,包括线索二叉树的遍历过程,可以实现低时间复杂度
-
并不是每个结点通过线索就能直接求出直接前驱和后继:
2.后序线索化,无法通过线索遍历所有结点,需要用到栈的支持(🐔💧)
前序:找前驱麻烦 后序:找后继麻烦(都需要知道父节点,三叉链表)
-
-
线索二叉树:为一种物理结构
二叉树为一种逻辑结构,但线索二叉树是加上线索后的链表线性表结构,即它是二叉树在计算机内部的一种存储结构,所以是一种物理结构
-
有序树:
树中任意节点的 子结点之间有顺序关系,这种树称为有序树
无序树:
树中任意节点的 子结点之间没有顺序关系,这种树称为无序树,也称为自由树
拓展 :递归
本章递归遍历极其重要‼️,是解决树问题的关键⚠️
-
递归可视化过程,对写出递归代码帮助大
Q&A:
1、对于三个结点A、B和C,可分别组成多少不同的无序树、有序树和二叉树?
可以组成 9 种无序树,分别是:
- 1个父节点,2个子结点的情况有 3 种
- 链式结构有 6 种(因为链式结构相互之间都是父子关系,所以有6种不同的组合)
有 12 种有序树,分别是:
- 1个父节点,2个子结点的情况有 6 种(子树可以交换位置)
- 链式结构有 6 种
组成的二叉树,30种,如下图所示每个6种(3!)
A / \ B C //第一种 A / B / C //第二种 A \ B \ C //第三种 A / B \ C //第四种 A \ B / C //第五种
2、如果哈夫曼树T有n₀个叶子结点,那么,树T有多少个结点?要求给出求解过程。
- 首先哈夫曼树,属于二叉树,即存在性质n₀=n₂+1,且哈夫曼树没有度为1的结点。
- 故n=2n₂+1=2(n₀-1)+1,即树T有2n₀-1个结点
3、设一棵完全二叉树采用顺序存储结构,保存在一维数组A中。试设计一个递归算法,复制该完全二叉树,得到一棵新的采用普通二叉链表存储的二叉树。设二叉链表的每个结点有3个域:lChild,rChild和element。算法返回所构造的新二叉树的根结点地址。
typedef struct TreeNode{ //结构体
int element;
TreeNode *lChild;
TreeNode *rChild;
TreeNode(int val) : element(val), lChild(nullptr), rChild(nullptr) {}
}TreeNode,*Tree;
Tree copyBuild(int A[],int n,int index){
if(index>=n) //如果下标超出数组范围return NULL
return null;
//创建树链结点
TreeNode *TNode = new TreeNode(A[index]);
//递归构造
Tnode->lChild=copyBuild(A,n,index*2+1);
Tnode->rChild=copyBuild(A,n,index*2+2);
//返回根结点
return Tnode;
}
4、已知二叉树以二叉链表存储,是编写算法输出二叉树中值为x的结点的所有祖先结点,假设值为x的结点不超过1个。
bool findAllParents(int x, LinkTree *T) {
if (T == null)
return false; // 空树返回,寻找失败
if (T->data == x)
return true; // 找到目标直接返回true
// 检查左子树或右子树中是否能找到目标节点
if ((T->data > x && findAllParents(x, T->lChild)) ||
(T->data < x && findAllParents(x, T->rChild))) {
print(T); // 打印路径祖先节点
return true; // 找到后返回true,停止继续递归
}
return false; // 如果在当前节点及其子树中未找到目标节点,返回false
}
5、设计一个算法,对二叉树进行按从上到下、从左到右的层次遍历。
//递归形式
void levelSearch(LinkTree *T, Queue &Q) {
if (T == null)
return; // 是空树,就返回
print(T); // 打印当前节点
// 将左右子树入队,前提是子树非空
if (T->lChild != null)
Q.Enqueue(T->lChild);
if (T->rChild != null)
Q.Enqueue(T->rChild);
// 队列非空,一个个出队,递归
if (!Q.IsEmpty()) {
LinkTree *nextNode = Q.Dequeue();
levelSearch(nextNode, Q);
}
}
//非递归,迭代形式,特别适用于广度优先遍历
void levelSearch(LinkTree *T) {
if (T == null)
return; // 空树直接返回
Queue Q;
Q.Enqueue(T); // 根节点入队
while (!Q.IsEmpty()) {
LinkTree *current = Q.Dequeue(); // 出队一个节点
print(current); // 打印当前节点
// 将当前节点的左右子节点入队
if (current->lChild != null)
Q.Enqueue(current->lChild);
if (current->rChild != null)
Q.Enqueue(current->rChild);
}
}
6、设一棵二叉树T采用二叉链表表示,试设计一个算法判断二叉树T是否为完全二叉树。
//算法思想:完全二叉树,即每个父节点可以有左孩子
//,或者同时拥有左右孩子,不存在只拥有右孩子
//递归形式
bool judge(LinkTree *T, Queue &Q)
{
if (T == null)
return true; // 空树直接返回
if (T->lChild == null && T->rChild != null)
return false; // 存在只有右孩子的节点,非完全二叉树
if (T->lChild != null)
Q.Enqueue(T->lChild);
if (T->rChild != null)
Q.Enqueue(T->rChild);
if (!Q.isEmpty())
{
LinkTree *nextTree = Q.Dequeue();
return judge(nextTree, Q); // 递归调用时返回结果
}
return true; // 队列为空,表示所有节点已处理完且符合条件
}
//迭代形式
bool judge(LinkTree *T)
{
if (T == null)
return true; // 空树直接返回
Queue Q;
Q.Enqueue(T);
while (!Q.isEmpty())
{
LinkTree *current = Q.Dequeue();
if (current->lChild == null && current->rChild != null) // 非完全二叉树
return false;
if (current->lChild != null) // 陆续入非空的左右子树
Q.Enqueue(current->lChild);
if (current->rChild != null)
Q.Enqueue(current->rChild);
}
return true; // 如果所有节点都符合条件,返回true
}
7、分别写出下面的算法
(1)在中序线索二叉树T中查找给定结点*p在中序序列中的前驱和后继。
//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
//循环找到最左下结点(不一定是叶结点)
while(p->ltag==0) p=p->lchild;
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p){
//右子树中最左下的结点
if(r->rtag==0) return Firstnode(p->rChild);
else return p->rChild; //rtag==1直接返回后继
}
-------------------------------------------------
//找到以p为根的子树中,最后被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
//循环找到最右下结点(不一定是叶结点)
while(p->rtag==0) p=p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱结点(前驱即右下)
ThreadNode *Prenode(ThreadNode *p){
//左子树中最右下的结点
if(r->ltag==0) return Firstnode(p->lChild);
else return p->lChild; //ltag==1直接返回前驱
}
(2)在先序线索二叉树T中查找给定结点*p在先序序列中的后继。
//在先序线索二叉树中找到结点p的后继结点
//ltag==0,有左子树,后继就是其左子树的根
//ltag==1,即没有左子树,根据先序遍历NLR,即后继就是右子树的根
ThreadNode *Nextnode(ThreadNode *p){
if(r->ltag==0) return p->lChild;
else return p->rChild; //rtag==1直接返回后继
}
(3)在后序线索二叉树T中查找给定结点*p在后序序列中的前驱。
//在后序线索二叉树中找到结点p的前驱结点
//1)ltag==0,有左子树,根据LRN,前驱结点比较复杂需要分析左右子树
//若右子树存在,即内判断rtag==0,寻找右子树最后一个后序遍历的结点,即右子树的根
//若右子树不存在,即rtag==1,寻找左子树最后一个后序遍历的结点,即左子树的根
//2)ltag==1,即没有左子树,根据线索化定义,其前驱就是lChild
ThreadNode *Prenode(ThreadNode *p) {
if (p->ltag == 0) { // 如果p有左子树
if (p->rtag == 0) { // 如果p有右子树
return p->rchild; // 右子树根结点即为前驱
} else { // 如果p没有右子树
return p->lchild; // 左子树根结点即为前驱
}
} else {
return p->lchild; // 没有左子树,直接返回lchild作为前驱
}
}
8、假设二叉树以二叉链表存储,设计一个算法,求其指定的某一层k(k>1)的叶子结点个数
//k>1,传入参数即是根结点时,第一层
int N0 = 0; //全局变量
void countN0(LinkTree *T, int k) {
if (T == nullptr) {
return; // 空节点直接返回
}
if (k == 1) {
// 如果当前层数是目标层,且是叶子节点
if (T->lChild == null && T->rChild == null) {
N0++;
}
return;
}
// 递归处理左子树和右子树,层数减1
countN0(T->lChild, k - 1);
countN0(T->rChild, k - 1);
}