考研数据结构复习408--树
注意:部分图来源于网络上,代码参考王道408数据结构
树的基本概念
树是n(n≥0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:
- 1)有且仅有一个特定的称为根的结点。
- 2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2,T3,其中每个集合本身又是一棵树,并且称为根结点的子树。
其实在树的定义中我们不难看出,树是一种递归的数据结构,也是分层结构。所以在后面我们可以进行对树进行递归遍历,层次遍历等操作。需要注意的是对于树而言,除了根节点之外,任何一个节点有且仅有一个前驱
。
一、基本术语
大佬可以直接跳过,看后面的线索二叉树
祖先节点
子孙,孩子,兄弟,堂兄弟如下图
- 节点的度和树的度
节点的孩子个数成为节点的度
树中节点最大的度数称为树的度 - 分支节点和叶节点
度>0的节点称为分支节点,度=0的节点称为叶节点 - 节点的深度,高度,层次
- 有序树和无序树
- 树和森林
森林是m(m>=0)个互不相交的树的集合, 可以是空森林
树的性质:
度为m的树,至少有一个节点的度是m
m叉树,每个节点最多m个孩子
度为m的树,第i
层至多有
m
i
−
1
m^{i-1}
mi−1个节点
高度为h,度为m的树 至少有h+m-1个结点
高度为h的m叉树至多有
(
m
h
−
1
)
/
(
m
−
1
)
(m^h-1)/(m-1)
(mh−1)/(m−1)个节点,至少有h个节点。
指定结点数的最小高度分析
度为m,具有n个节点的树的最小高度h为
⌈
l
o
g
m
(
n
(
m
−
1
)
+
1
)
⌉
\lceil log_m(n(m-1) + 1) \rceil
⌈logm(n(m−1)+1)⌉向上取整,推导:
(
m
(
h
−
1
)
−
1
)
/
(
m
−
1
)
<
n
⩽
(
m
h
)
/
(
m
−
1
)
(m^(h-1^)-1)/(m-1) < n ⩽ (m^h)/(m-1)
(m(h−1)−1)/(m−1)<n⩽(mh)/(m−1)
以上内容出题时需注意:
设n0, n1, n2, … nm表示度为m的节点的个数
- 1、总结点数 = n0+n1+n2+…+nm
- 2、总分支数 = 1 * n1 + 2 * n2 + …+ m * nm
- 3、总结点数 = 总分支数 + 1
二、 二叉树的概念(包含代码实现,线索二叉树)
2.1、二叉树的定义与主要特性
2.1.1、定义
!!! 二叉树是有序树
属于树的一种特例,也采用递归的形式定义。
二叉树是n(n≥0)个结点的有限集合:
- ①或者为空二叉树,即n=0。
- ②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点
:①每个结点至多只有两棵子树②左右子树不能颠倒(二叉树是有序树)注意区别:度为2的有序树
2.1.2、特殊的二叉树
-
二叉排序树:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字:
- 右子树上所有结点的关键字均大于根结点的关键字。
- 左子树和右子树又各是一棵二叉排序树。
二叉排序树可用于元素的排序、搜索
-
平衡二叉树: 树上的任一节点的左子树和右子树的深度之差不超过1
-
正则二叉树:树中只有度为0或2的节点
2.2、二叉树的性质
二叉树的常考性质:
-
常见考点1:设非空二叉树中度为0、1和2的结点个数分别为no、n1和n2,则n0=n2+1(叶子结点比二分支结点多一个)
假设树中结点总数为n,则 ①n=no+n1+n2 ②n=n1+2n2+1 树的结点数=总度数+1 ==============>>>>no=n2+1
-
常见考点2:二叉树第i层至多有 2 ( i − 1 ) 2^(i-1^) 2(i−1)个结点(i≥1) m叉树第i层至多有 m ( i − 1 ) m^(i-1^) m(i−1)个结点(i≥1)
-
常见考点3:高度为h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点(满二叉树)高度为h的m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h-1)/ (m-1) (mh−1)/(m−1)个结点。 等比数列求和
完全二叉树的性质:
- 常见考点1:基于n个(n>0)节点的完全二叉树的高度h为 ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n + 1)\rfloor ⌊log2(n+1)⌋ 或者 ⌊ l o g 2 n + 1 ⌋ \lfloor log_2n + 1\rfloor ⌊log2n+1⌋
- 常见考点2:对于完全二叉树,可以由的结点数n推出度为0、1和2的结点个数为no、n1和n2。
完全二叉树最多只有一个度为1的结点,即n1=0或1 no=n2+1→n0+n2 一定是奇数
若完全二叉树有2k个(偶数)个结点,则必有n1=1,n0=k,n2=k-1
若完全二叉树有2k-1个(奇数)个结点,则必有n1=0,n0=k,n2=k-1
2.3、二叉树的存储结构
2.3.1、顺序存储结构
满二叉树和完全二叉树采用顺序存储比较合适
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 100 // 定义二叉树能存储的最大节点数
// 定义二叉树结构体
typedef struct {
char data[MAXSIZE]; // 存储节点数据的数组
int count; // 记录二叉树节点个数
} BinaryTree;
// 初始化二叉树
void initBinaryTree(BinaryTree *tree) {
tree->count = 0;
}
// 插入节点
void insertNode(BinaryTree *tree, char data) {
if (tree->count < MAXSIZE) {
tree->data[tree->count] = data;
tree->count++;
} else {
printf("二叉树已满,无法插入新节点\n");
}
}
// 获取左孩子节点
char getLeftChild(BinaryTree *tree, int index) {
int leftChildIndex = 2 * index + 1;
if (leftChildIndex >= tree->count) {
return '\0';
} else {
return tree->data[leftChildIndex];
}
}
// 获取右孩子节点
char getRightChild(BinaryTree *tree, int index) {
int rightChildIndex = 2 * index + 2;
if (rightChildIndex >= tree->count) {
return '\0';
} else {
return tree->data[rightChildIndex];
}
}
int main() {
BinaryTree binaryTree;
initBinaryTree(&binaryTree);
insertNode(&binaryTree, 'A');
insertNode(&binaryTree, 'B');
insertNode(&binaryTree, 'C');
insertNode(&binaryTree, 'D');
insertNode(&binaryTree, 'E');
insertNode(&binaryTree, 'F');
printf("节点A的左孩子节点是:%c\n", getLeftChild(&binaryTree, 0));
printf("节点A的右孩子节点是:%c\n", getRightChild(&binaryTree, 0));
printf("节点B的左孩子节点是:%c\n", getLeftChild(&binaryTree, 1));
printf("节点B的右孩子节点是:%c\n", getRightChild(&binaryTree, 1));
printf("节点C的左孩子节点是:%c\n", getLeftChild(&binaryTree, 2));
printf("节点C的右孩子节点是:%c\n", getRightChild(&binaryTree, 2));
return 0;
}
2.3.2、链式存储结构
由于顺序存储空间利用率低,因此一般都用链式存储,就会造成n+1个空链域,可构成线索二叉树
2.4 二叉树的遍历与线索二叉树
2.4.1 二叉树的遍历
遍历:按照某种次序把所有节点都访问一遍
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild; //声明哪种类型的指针
}BiTnode, *BiTree; // 起别名相当于
BiTnode
:是struct BiTNode
的别名。这意味着你可以使用BiTnode
来代替struct BiTNode
。BiTree
:是struct BiTNode *
(即指向BiTNode
的指针)的别名。这意味着你可以使用BiTree
来声明一个指向二叉树节点的指针。
- 先序遍历 (根左右)
若二叉树为空,什么也不做
非空根左右void PreOrder(BiTree T){ if(T!=Null){ visit(T); PreOrder(T->lchild); PreOrder(T->rchild); } }
- 中序遍历(左根右)
void INOrder(BiTree T){ if(T!=Null){ PreOrder(T->lchild); visit(T); PreOrder(T->rchild); } }
- 后序遍历(左右根)
void PreOrder(BiTree T){ if(T!=Null){ PreOrder(T->lchild); PreOrder(T->rchild); visit(T); } }
现在我们已经知道了基本的遍历方法应用一下吧!
求树的深度,可对照下图进行模拟
int treeDepth(BiTree T){
if (T == Null){
return 0;
}else{
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
return l>r ? l+1 : r+1;
}
}
2.4.2 遍历序列构造二叉树
结论:若只给出一棵二叉树的前/中/后/层 序遍历序列中的一种,不能唯一确定一棵二叉树
-
前序+中序
语言描述就是:前序遍历的第一个节点是根节点,在中序遍历序列中可以根据根节点将其分为左子树和右子树。然后对齐左子树和右子树重复这个工程即可。
知道这个流程就请练习一下吧
答案:
-
后序+中序
在熟练掌握上一个之后,我想这个就更简单了,需要注意的是后续遍历的最后一个节点是根节点 -
层序+中序
总结:
2.4.3 线索化二叉树
滴滴,未来的准研究生们请注意啦:线索化二叉树的前驱和后继值得是遍历序列中的前驱和后继!!!
线索化,前面提到过在使用链式存储时会多出n+1个指针域是空的,我们在找中序前驱,土办法从根节点遍历,使用一个Pre来存储前驱
土办法的代码奉上:
void findPre(BiTree T){
if(T!=Null){
findPre(T->lchild);
visit(T);
findPre(T->rchild);
}
}
void visit(BiTnode * q){
if (q == p)
final = pre;
else
pre = q;
}
BiTNode *p;
BiTNode * pre= NULL;
BiTNode * final = NULL;
难搞呀,要从头开始遍历,怎么办? 我们有n+1个空链域呀,要充分使用起来啦。所以线索化二叉树就来了。
增加ltag 和 rtag去记录每个结点的左右指针,为1时 ltag 前驱, rtag后继 0则代表的左右孩子。
所以初始化代码奉上,与原来定义二叉树的地方多了两个flag.
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode * lchild, * rchild
int ltag, rtag;//左、右线索标志
}ThreadNode, *ThreadTree;
很好,我们现在已经初始化了,那我们就启动吧。
- 中序线索化(
中序遍历二叉树,一边遍历,一边线索化
)
ThreadNode *pre = NULL;
void InThread(ThreadTree T){
if (T != NULL){
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchild ==NuLl){//左子树为空,建立前驱线索
q->Lchild=pre;
q->Ltag=1;}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild =q;//建立前驱结点的后继线索
pre->rtag=1;
}
pre=q;
}
代码执行逻辑结构,先判断左子树建立前驱线索,以及前驱节点的右子树为其建立后继线索
已经知道代码了,下面我们结合图示手推一遍线索化的流程。对于上图,首先使用Pre = NULL作用是第一个节点没有直接前驱,我们需要将其置为NULL,我们进行按照左根右
的顺序中序遍历,访问第一个节点D,其左孩子为空为其建立前驱线索,又因为并没有其前驱元素所以执行q->lcild=pre;将其左孩子置为NULL和ltag=1。pre为空没有后继,pre = D。接着继续遍历G节点左右孩子均无,前驱指向D,D有右孩子无后继,pre=G。遍历B,有左孩子,pre无右孩子,将pre也就是G的右孩子指针指向B…
- 先序线索化(注意添加判断解决转圈圈)
visit时,若节点没有左孩子,就会将节点的左孩子指针指向前驱节点,从而造成转圈的问题
2.4.4、在线索二叉树中找前驱后继