1
0
mirror of https://github.com/Didnelpsun/CS408.git synced 2026-02-09 13:45:48 +08:00

更新树

This commit is contained in:
Didnelpsun
2021-09-22 23:32:59 +08:00
parent 95f6306633
commit 7d7e4e446b
7 changed files with 254 additions and 38 deletions

View File

@@ -6,7 +6,7 @@ typedef struct LinkStackNode{
element_type data;
// 指针
LinkStackNode *next;
} LinkStackNode, *LinkStack;
} *LinkStack;
// 初始化
LinkStack InitLinkStack(){

View File

@@ -1,7 +1,139 @@
#include "head.h"
#include <stack>
#include <queue>
// 二叉树结点
typedef struct BinaryTreeNode {
element_type data;
BinaryTreeNode *left_child, *right_child;
} BinaryTreeNode, *BinaryTree;
} BinaryTreeNode, *BinaryTree;
// 前序遍历
bool PreOrder(BinaryTree &tree, bool( *function)(BinaryTree&)){
if(tree!= nullptr){
if(!function(tree))
return false;
PreOrder(tree->left_child, function);
PreOrder(tree->right_child, function);
}
return true;
}
// 中序遍历
bool InOrder(BinaryTree &tree, bool(* function)(BinaryTree&)){
if(tree!= nullptr){
PreOrder(tree->left_child, function);
if(!function(tree))
return false;
PreOrder(tree->right_child, function);
}
return true;
}
// 后序遍历
bool PostOrder(BinaryTree &tree, bool(* function)(BinaryTree&)){
if(tree!= nullptr){
PreOrder(tree->left_child, function);
PreOrder(tree->right_child, function);
if(!function(tree))
return false;
}
return true;
}
// 非递归先序遍历
bool PreOrderNonRecursive(BinaryTree &tree, bool(* function)(BinaryTree&)){
// 初始化栈栈元素为tree指针
std::stack<BinaryTree> stack;
// 赋值一个新树
BinaryTree new_tree = tree;
// 如果栈不空或new_tree不空时循环
while(new_tree||!stack.empty()){
// 一直向左
if(new_tree){
// 访问当前结点
if(!function(new_tree)){
printf("PreOrderNonRecursive:访问结点失败!");
return false;
}
// 当前结点入栈
stack.push(new_tree);
// 左孩子不空则一直向左
new_tree = new_tree->left_child;
}
// 出栈,并转向右子树
else{
// 返回栈顶的元素,但不删除该元素
new_tree = stack.top();
// 删除栈顶元素但不返回其值
stack.pop();
// 向右
new_tree = new_tree->right_child;
// 返回while
}
}
return true;
}
// 非递归中序遍历
bool InOrderNonRecursive(BinaryTree &tree, bool(* function)(BinaryTree&)){
// 初始化栈栈元素为tree指针
std::stack<BinaryTree> stack;
// 赋值一个新树
BinaryTree new_tree = tree;
// 如果栈不空或new_tree不空时循环
while(new_tree||!stack.empty()){
// 一直向左
if(new_tree){
// 当前结点入栈
stack.push(new_tree);
// 左孩子不空则一直向左
new_tree = new_tree->left_child;
}
// 出栈,并转向右子树
else{
// 返回栈顶的元素,但不删除该元素
new_tree = stack.top();
// 删除栈顶元素但不返回其值
stack.pop();
// 访问出栈结点
if(!function(new_tree)){
printf("InOrderNonRecursive:访问结点失败!");
return false;
}
// 向右
new_tree = new_tree->right_child;
// 返回while
}
}
return true;
}
// 层序遍历
bool LevelOrder(BinaryTree &tree, bool(* function)(BinaryTree&)){
// 初始化辅助队列
std::queue<BinaryTree> queue;
// 初始化树结点
BinaryTree new_tree=tree;
// 根结点入队
queue.push(new_tree);
// 若队列非空则循环
while(!queue.empty()){
// 队头结点出队
new_tree = queue.front();
queue.pop();
// 访问出队结点
if(!function(new_tree)){
printf("LevelOrder:访问结点失败!");
return false;
}
// 如果左子树不空则其根结点入队
if(new_tree->left_child!= nullptr){
queue.push(new_tree);
}
if(new_tree->right_child!= nullptr){
queue.push(new_tree);
}
}
return true;
}

View File

@@ -1,7 +1,7 @@
#include "head.h"
// 顺序栈
typedef struct {
typedef struct SequenceStack {
// 栈内元素
element_type *data;
// 栈顶指针

View File

@@ -0,0 +1,11 @@
#include "head.h"
// 线索二叉树结点
typedef struct ThreadTreeNode{
// 数据
element_type data;
// 左右孩子结点
ThreadTreeNode *left_child, *right_child;
// 左右线索指针
int left_tag, right_tag;
} ThreadTreeNode, *ThreadTree;

View File

@@ -12,6 +12,7 @@
#include "../head/sequence_string.h"
#include "../head/link_string.h"
#include "../head/link_tree.h"
#include "../head/thread_tree.h"
using namespace std;

View File

@@ -13,3 +13,57 @@ $C.113$
$D.122$
解:$B$。设树中度为$i$$i=0,1,2,3,4$)的结点数分别为$n_i$,树中结点总数为$n$,则$n$=分支数$+1$,而分支数又等于树中各结点的度之和,即$n=1+0n_0+n_1+2n_2+3n_3+4n_4=n_0+n_1+n_2+n_3+n_4$。依题意,$n_1+2n_2+3n_3+4n_4=10+2+30+80= 122$$n_1+n_2+n_3+n_4=10+1+10+20=41$,可得出$n_0=82$,即树$T$的叶结点的个数是$82$。
## 二叉树
### 二叉树性质
**例题** 下列关于二叉树的说法中,正确的是()。
$A.$度为$2$的有序树就是二叉树
$B.$含有$n$个结点的二叉树的高度$\lfloor\log_2n\rfloor+1$
$C.$在完全二叉树中,若一个结点没有左孩子,则它必是叶结点
$D.$在任意一棵非空二叉排序树中,删除某结点后又将其插入,则所得二叉排序树与删除前原二叉排序树相同
解:$C$。在二叉树中,若某个结点只有一个孩子,则这个孩子的左右次序是确定的,而在度为$2$的有序树中,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,$A$错误。选项$B$仅当是完全二叉树时才有意义,对于任意一棵二叉树,高度可能为$\lfloor\log_2n\rfloor+1\sim n$。在二叉排序树中插入结点时,一定插入在叶结点的位置,故若先删除分支结点再插入,则会导致二叉排序树的重构,其结果就不再相同,$D$错误。根据完全二叉树的定义,在完全二叉树中,若有度为$1$的结点,则只可能有一个,且该结点只有左孩子而无右孩子,选项$C$正确。
**例题** 设二叉树有$2n$个结点,且$m<n$,则不可能存在()的结点。
$A.n$个度为$0$
$B.2m$个度为$0$
$c.2m$个度为$1$
$D.2m$个度为$2$
解:$C$。由二叉树的性质可知$n_0=n_2+1$,结点总数$=2n=n_0+n_1+n_2=n_1+2n_2+1$,则$n_1=2(n-n_2)-1$,所以$n$为奇数,说明该二叉树中不可能有$2m$个度为$1$的结点。
**例题** 已知一棵完全二叉树的第$6$层(设根为第$1$层)有$8$个叶结点,则该完全二叉树的结点个数最多是()。
$A.39$
$B.52$
$C.111$
$D.119$
解:$C$。第$6$层有叶结点,完全二叉树的高度可能为$6$或$7$,显然树高为$7$时结点最多。完全二叉树与满二叉树相比,只是在最下一层的右边缺少了部分叶结点,而最后一层之上是个满二叉树,并且只有最后两层上有叶结点。若第$6$层上有$8$个叶结点,则前$6$层为满二叉树,而第$7$层缺失了$8\times2=16$个叶结点,故完全二叉树的结点个数最多为$2^7-1-16=111$。
**例题** 一棵有$124$个叶子结点的完全二叉树,最多有()个结点。
$A.247$
$B.248$
$C.249$
$D.250$
解:$B$。在非空的二叉树当中,由度为$0$和$2$的结点数的关系$n_0=n_2+1$可知$n_2=123$;总结点数$n=n_0+n_1+n_2=247+n_1$,其最大值为$248$(由于完全二叉树$n_1$的取值为$1$或$0$,当$n=1$时结点最多)。注意,由完全二叉树总结点数$n$的奇偶性可以确定$n_1$的值,但不能根据$n_0$来确定的$n_1$值。
### 二叉树遍历

View File

@@ -78,7 +78,7 @@
如果要保存父结点的位置,可以添加一个父结点指针,从而变成三叉链表。
### 二叉树遍历
### 二叉树遍历
遍历是按照某种次序将所有结点都访问一遍。
@@ -86,14 +86,29 @@
顺序遍历就是深度优先的遍历,分为三种:
+ 先序遍历根左右NLR。
+ 中序遍历左根右LNR。
+ 后序遍历左右根LRN。
+ 先序遍历:根左右$NLR$
+ 中序遍历:左根右$LNR$
+ 后序遍历:左右根$LRN$
根据算算数表达式的分析树的不同先序、中序、后序遍历方式可以得到前缀、中缀、后缀表达式。
若树的高度为$h$,则时间复杂度为$O(h)$。
#### 递归与非递归
借助栈可以将本来是递归算法的顺序遍历变为非递归方式。
如中序遍历:
1. 沿着根的左孩子结点依次入栈,直到左孩子为空。表示找到了最左边的可以输出的结点。
2. 栈顶元素出栈并访问。
3. 若栈顶元素的右孩子为空,则继续执行步骤二。
4. 若栈顶元素的右孩子不为空,则对其右子树执行步骤一。
先序遍历与中序遍历类似,只是第一步就需要访问中间结点。
后序非递归遍历算法的思路:从根结点开始,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,但是此时不能出栈并访问,因为如果其有右子树,还需按相同的规则对其右子树进行处理。直至上述操作进行不下去,若栈顶元素想要出栈被访问,要么右子树为空,要么右子树刚被访问完(此时左子树早已访问完),这样就保证了正确的访问顺序。
#### 层序遍历
层序遍历就是广度优先的遍历。
@@ -107,7 +122,7 @@
若只给出一棵二叉树的前/中/后/层序遍历序列中的一种不能唯一确定一棵二叉树。只有给出中序遍历序列才可能推出唯一二叉树,因为无法确定根结点相对于左右结点的位置:
+ 前序+中序
+ 前序+中序
+ 后序+中序。
+ 层序+中序。
@@ -115,6 +130,8 @@
前序:根+左+右;中序:左+根+右。所以根据三个部分对应相同可以推出。
先序遍历的第一个结点一定是二叉树根节点。中序遍历中根结点必然将序列分为两个部分,前一个序列是左子树的中序序列,后一个序列是右子树的中序序列。同理先序序列中子序列的第一个结点就是左右子树的根节点。
#### 后序+中序
后序:左+根+右;中序:左+根+右。所以根据三个部分对应相同可以推出。
@@ -125,51 +142,52 @@
### 线索二叉树
对于二叉树的遍历,只能从根结点开始遍历,如果给任意一个结点是无法完成遍历的。
对于二叉树的遍历,只能从根结点开始遍历,如果给任意一个结点是无法完成遍历的。一般的二叉树的左右结点是用来表示父子关系,而不能体现遍历关系。
所以我们就想能否保存结点的前驱和后继,从而能减少重复遍历树。因为一棵树很多结点的左右结点可能是空的,那么这些空闲的指针可以不代表左右子树的根结点,而是用来表示当前遍历方法的前驱或后继。当这个指针表示的是前驱或后继就称为线索,指向前驱的就是前驱线索,由左孩子指针担当,指向后继的就是后继线索,由右孩子指针担当。
#### 线索化
为了区分其左右孩子指针是指向什么要在结点中新建两个tag位如当ltag=0表示lchild指向的是左孩子结点而为1表示其指向前驱。
为了区分其左右孩子指针是指向什么,要在结点中新建两个$tag$位,如当$ltag=0$表示$lchild$指向的是左孩子结点,而为$1$表示其指向前驱。
+ 确定线索二叉树类型——中序、先序或后序。
+ 按照对应遍历规则,确定每个结点访问顺序并写上编号。
+ 将n+1个空链域连上前驱后继。
+ 将$n+1$个空链域连上前驱后继。
+ 这种结构称为线索链表。
#### 查找前驱后继
+ 中序线索二叉树中找到结点*P的中序后继next
+ 若p右孩子指针指向后继p->rtag==1则next=p->rchild。
+ 若p右孩子指针指向右子树根结点p->rtag==0则next=p右子树中最左下结点。
+ 中序线索二叉树中找到结点$*P$的中序后继$next$
+ 若$p$右孩子指针指向后继:$p->rtag==1$,则$next=p->rchild$
+ 若$p$右孩子指针指向右子树根结点:$p->rtag==0$,则$next=p$右子树中最左下结点。
+ 所以可以利用线索对二叉树实现非递归的中序遍历。
+ 中序线索二叉树中找到结点*P的中序前驱pre
+ 若p左孩子指针指向前驱p->ltag==1则pre=p->lchild。
+ 若p左孩子指针指向左子树根结点p->ltag==0则pre=p左子树中的最右下结点。
+ 中序线索二叉树中找到结点$*P$的中序前驱$pre$
+ 若$p$左孩子指针指向前驱:$p->ltag==1$,则$pre=p->lchild$
+ 若$p$左孩子指针指向左子树根结点:$p->ltag==0$,则$pre=p$左子树中的最右下结点。
+ 所以可以利用线索对二叉树实现非递归的逆向中序遍历。
+ 先序线索二叉树中找到结点*P的先序后继next
+ 若p右孩子指针指向后继p->rtag==1则next=p->rchild。
+ 若p右孩子指针指向右子树根结点p->rtag==0如果p有左孩子则p->next=p->lchild如果p没有左孩子则p->next=p->rchild。
+ 先序线索二叉树中找到结点$*P$的先序后继$next$
+ 若$p$右孩子指针指向后继:$p->rtag==1$,则$next=p->rchild$
+ 若$p$右孩子指针指向右子树根结点:$p->rtag==0$,如果$p$有左孩子,则$p->next=p->lchild$,如果$p$没有左孩子,则$p->next=p->rchild$
+ 所以可以利用线索对二叉树实现非递归的先序遍历。
+ 先序线索二叉树中找到结点*P的中序前驱pre
+ 若p左孩子指针指向前驱p->ltag==1则pre=p->lchild。
+ 若p左孩子指针指向左子树根结点p->ltag==0先序遍历中左右子树的根结点只可能是后继所以这时候就找不到p的前驱,如果没有父结点只能从头开始先序遍历。
+ 先序线索二叉树中找到结点$*P$的中序前驱$pre$
+ 若$p$左孩子指针指向前驱:$p->ltag==1$,则$pre=p->lchild$
+ 若$p$左孩子指针指向左子树根结点:$p->ltag==0$,先序遍历中左右子树的根结点只可能是后继,所以这时候就找不到$p$的前驱,如果没有父结点只能从头开始先序遍历。
+ 如果有父结点,则又有三种情况:
+ p为左孩子,则根据根左右,p的父结点为根所以在p的前面p->pre=p->parent。
+ p为右孩子其左兄弟为空则根据根左右顺序为根右所以p->pre=p->parent。
+ p为右孩子且有左兄弟根据根左右p的前驱就是左兄弟子树中最后一个被先序遍历的结点即在p的左兄弟子树中优先右子树遍历的底部。
+ 后序线索二叉树中找到结点*P后中序后继next
+ 若p右孩子指针指向后继p->rtag==1则next=p->rchild。
+ 若p右孩子指针指向右子树根结点p->rtag==0则根据左右根顺序左右孩子结点必然是p的前驱而不可能是后继,所以找不到后序后继,如果没有父结点只能使用从头开始遍历的方式。
+ $p$为左孩子,则根据根左右,$p$的父结点为根所以在$p$的前面,$p->pre=p->parent$
+ $p$为右孩子,其左兄弟为空,则根据根左右,顺序为根右,所以$p->pre=p->parent$
+ $p$为右孩子且有左兄弟根据根左右p的前驱就是左兄弟子树中最后一个被先序遍历的结点即在p的左兄弟子树中优先右子树遍历的底部。
+ 后序线索二叉树中找到结点$*P$后中序后继$next$
+ 若$p$右孩子指针指向后继:$p->rtag==1$,则$next=p->rchild$
+ 若$p$右孩子指针指向右子树根结点:$p->rtag==0$,则根据左右根顺序,左右孩子结点必然是$p$的前驱而不可能是后继,所以找不到后序后继,如果没有父结点只能使用从头开始遍历的方式。
+ 如果有父结点则又有三种情况:
+ p为右孩子根据左右根所以p->next=p->parent。
+ p为左孩子右孩子为空根据左右根所以p->next=p->parent。
+ p为左孩子右孩子非空根据左右根所以p->next=右兄弟子树中第一个被后序遍历的结点。
+ 后序线索二叉树中找到结点*P后中序前驱pre
+ 若p左孩子指针指向前驱p->ltag==1则pre=p->lchild。
+ 若p左孩子指针指向左子树根结点p->ltag==0则又有两种情况
+ 若p有右孩子则按照左右根的情况遍历右在根的前面所以p->pre=p->rchild。
+ 若p没有右孩子按照左根的顺序则p->pre=p->lchild。
+ $p$为右孩子,根据左右根,所以$p->next=p->parent$
+ $p$为左孩子,右孩子为空,根据左右根,所以$p->next=p->parent$
+ $p$为左孩子,右孩子非空,根据左右根,所以$p->next=$右兄弟子树中第一个被后序遍历的结点。
+ 后序线索二叉树中找到结点$*P$后中序前驱$pre$
+ 若$p$左孩子指针指向前驱:$p->ltag==1$,则$pre=p->lchild$
+ 若$p$左孩子指针指向左子树根结点:$p->ltag==0$,则又有两种情况:
+ 若$p$有右孩子,则按照左右根的情况遍历,右在根的前面,所以$p->pre=p->rchild$
+ 若$p$没有右孩子,按照左根的顺序,则$p->pre=p->lchild$
### 二叉排序树
@@ -450,7 +468,7 @@ AL(H) CL(H-1) CR(H) BR(H)
### 树的存储结构
+ 双亲表示法:是一种顺序存储方式,每个结点中保存指向双亲的指针。查找双亲方便,但是查找孩子就只能从头遍历。
+ 孩子表示法是顺序加链式存储方法顺序存储所有元素添加一个firstChild域指向第一个孩子结构体的指针孩子结构体包括元素位置索引与指向下一个孩子结构体的next指针。
+ 孩子表示法是顺序加链式存储方法顺序存储所有元素添加一个firstChild域指向第一个孩子结构体的指针孩子结构体包括元素位置索引与指向下一个孩子结构体的$next$指针。
+ 孩子兄弟表示法:是一种链式存储方式,定义了两个指针,分别指向第一个孩子与右兄弟,类似于二叉树,可以利用二叉树来实现对树的处理 。
### 森林与树的转换