mirror of
https://github.com/Didnelpsun/CS408.git
synced 2026-02-09 13:45:48 +08:00
更新树
This commit is contained in:
@@ -6,7 +6,7 @@ typedef struct LinkStackNode{
|
||||
element_type data;
|
||||
// 指针
|
||||
LinkStackNode *next;
|
||||
} LinkStackNode, *LinkStack;
|
||||
} *LinkStack;
|
||||
|
||||
// 初始化
|
||||
LinkStack InitLinkStack(){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "head.h"
|
||||
|
||||
// 顺序栈
|
||||
typedef struct {
|
||||
typedef struct SequenceStack {
|
||||
// 栈内元素
|
||||
element_type *data;
|
||||
// 栈顶指针
|
||||
|
||||
11
Code/Code/head/thread_tree.h
Normal file
11
Code/Code/head/thread_tree.h
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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$值。
|
||||
|
||||
### 二叉树遍历
|
||||
@@ -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$指针。
|
||||
+ 孩子兄弟表示法:是一种链式存储方式,定义了两个指针,分别指向第一个孩子与右兄弟,类似于二叉树,可以利用二叉树来实现对树的处理 。
|
||||
|
||||
### 森林与树的转换
|
||||
|
||||
Reference in New Issue
Block a user