树与二叉树知识点总结(一)
树
树的定义
树是\(N(N>0)\)个结点的有限集合,\(N=0\)时,称为空树,这是一种特殊情况,在任意一颗非空树中应满足:
- 有且仅有一个特定的称为根的结点
- 当\(N>1\)时,其余结点可分为\(m(m>0)\)个互不相交的有限集合\(T_1,T_2,……,T_m\),其中每个集合本身又是一棵树,并且称为根结点的子树
通俗理解总结:
- 有且仅有一个根结点
- 根结点没有前驱结点,其他结点有且只有一个前驱结点
- 所有结点可以有零个或者多个后继结点
- 树是一种递归的数据结构,适合于表示所有层次结构的数据
树的基本术语
-
结点关系
- 祖先结点:根结点到该结点的唯一路径的任意结点
- 子孙结点
- 双亲结点:根结点到该结点的唯一路径上最接近该结点的结点
- 孩子结点
- 兄弟结点:具有相同双亲结点的结点
-
树的度:树中所有结点的度数的最大值
-
结点的层次、深度、高度
- 层次:根为第一层、它的孩子为第二层,以此类推
- 深度:根结点开始自顶向下累加
- 高度:叶子结点开始自底向上累加
-
树的高度(深度)、路径、路径长度
-
树的高度(深度):树中结点的最大层数
-
路径:又两个结点之间所经过的结点序列构成的
-
路径长度:路径上所经过的边的个数
由于树种的分支是有向的,即从双亲指向孩子,所以数中的路径是自上而下的,同一双亲的两个孩子之间不存在路径
-
树的性质
- 树中的结点数等于所有结点的度数加1。
证明:
不难想象,除根结点以外,每个结点有且仅有一个指向它的前驱结点。也就是说每个结点和指向它的分支一一对应。
假设树中一共有\(b\)个分支,那么除了根结点,整个树就包含有\(b\)个结点,所以整个树的结点数就是这\(b\)个结点加上根结点,设为\(n\),则\(n=b+1\)。而分支数\(b\)也就是所有结点的度数,证毕。
- 度为\(m\)的树中第\(i\)层上至多结点树如下
证明:(数学归纳法)
首先考虑\(i=1\)的情况:第一层只有根结点,即一个结点,\(i=1\)带入式子满足。
假设第\(i-1\)层满足这个性质,第\(i-1\)层最多有\(m^{i-2}\)个结点,又因为树的度为\(m\),所以对于第\(i-1\)层的每个结点,最多有\(m\)个孩子结点。所以第\(i\)层的结点数最多是\(i-1\)层的\(m\)倍,所以第\(i\)层上最多有\(m ^{i-1}\)个结点。
- 高度为\(h\)的\(m\)叉树至多的结点数如下
- 具有\(n\)个结点的\(m\)叉树的最小高度如下
- 树结点与度之间的关系有
树的存储结构
顺序存储结构
双亲表示法:用一组连续的存储空间存储树的结点,同时在每个结点中,用一个变量存储该结点的双亲结点在数组中的位置。
如图所示:
代码如下:
typedef char ElemType;
typedef struct TNode{
ElemType data; //结点数据
int parent; //该结点双亲在数组中的下标
}Tnode; //结点数据类型
#define MaxSize 100
typedef struct{
TNode nodes[MaxSize]; //结点数组
int n; //结点数量
}Tree; //树的双亲表示结构
优点:可以很快得到每个结点的双亲结点
缺点:求结点的孩子需要遍历整个结构
链式存储结构
孩子表示法:
把每个结点的孩子结点排列起来存储成一个单链表。所以\(n\)个结点就有\(n\)个链表;
如果是叶子结点,那这个结点的孩子单链表就是空的;
然后\(n\)个单链表的的头指针又存储在一个顺序表(数组)中。
如图所示:
代码如下:
typedef char ElemType;
typedef struct CNode{
int child; //该孩子在表头数组的下标
struct CNode *next; //指向该结点的下一个孩子结点
}CNode,*Child; //孩子结点的数据类型
typedef struct{
ElemType data; //结点数据域
Child firstchild; //指向该结点的第一个孩子结点
}TNode; //孩子结点的数据类型
优点:寻找子女非常直接
缺点:寻找双亲需要便利\(N\)个结点的孩子链表指针域所只想的\(N\)个孩子链表
孩子兄弟表示法:
孩子兄弟表示法:顾名思义就是要存储孩子和孩子结点的兄弟,具体来说,就是设置两个指针,分别指向该结点的第一个孩子结点和这个孩子结点的右兄弟结点。
如图所示:
代码如下:
typedef char ElemType;
typedef struct CSNode{
ElemType data; //结点数据域
struct CSNode *firstchild,*rightsib; //指向该结点的第一个孩子结点和该结点的右兄弟结点
}CSNode; //孩子兄弟点的数据类型
优点:方便实现转换为二叉树,易于查找结点的孩子
缺点:从当前结点查找其双亲结点比较麻烦
二叉树
二叉树的定义
二叉树是\(n(n≥0)\)个结点的有限集合:
- 或者为空二叉树,即 \(n=0\)
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
通俗理解:
每个结点至多有两颗子树,左右子树的顺序不能颠倒,二叉树与度为2的有序树不同,不同的原因是度为2的树要求每个结点最多只能有两棵子树,并且至少有一个结点有两棵子树。二叉树的要求是度不超过2,结点最多有两个叉,可以是1或者0。
二叉树的五种基本形态
- 空树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
拥有特殊形态的二叉树
- 斜树:每个结点只有左结点或者每个结点只有右结点
- 满二叉树:树种每一层都含有最多的结点,对于编号\(i\)的结点,期双亲结点为\(\lfloor i/2\rfloor\)
- 完全二叉树:每一个结点都与高度为h的满二叉树编号\(1-n\)相同;如果\(i≤n/2\)下,则结点\(i\)为分支结点,否则为叶子结点
- 二叉排序树:左子树均小于根结点,右子树均大于根结点
- 平衡二叉树:左右子树的深度之差不超过1
二叉树的性质
- 非空二叉树上的叶子结点数等于度为2的结点数加一,即 \(n_0=n_2+1\)
- 非空二叉树上第\(k\)层上至多有\(2^{k-1}\)个结点\((k≥1)\)
- 高度为\(h\)的二叉树至多有\(2^k - 1\)个结点\((h≥1)\)
- 具有\(n\)个\((n>0)\)结点的完全二叉树的高度为\(\lceil log_2{n+1}\rceil\)或\(\lfloor log_2n\rfloor+1\)
二叉树的存储结构
顺序存储结构
二叉树的顺序存储结构就是用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素。
如图所示:
优点:适合完全二叉树和满二叉树,序号可以反映出结点之间的逻辑关系,可以节省空间
缺点:适合一般二叉树,只能添加一些空结点,空间利用率低
链式存储结构
二叉树每个结点最多两个孩子,所以设计二叉树的结点结构时考虑两个指针指向该结点的两个孩子。
如图所示:
代码如下:
typedef char ElemType;
typedef struct BiTNode{
Elemtype data;
struct BiTNode *lchild,*rchild;
}
二叉树的遍历
先序遍历(\(NLR\))
过程:
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
代码如下:
递归代码如下:
void PreOrder(BiTree T)
{
//先序遍历算法
if(T!=NULL){
vist(T); //访问根结点,如:printf("%c",T->data);
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); ////递归遍历右子树
}
}
非递归代码如下
void Preorder2(BiTree T){
//先序遍历非递归算法
InitStack(S); //需要借助一个递归栈
BiTree p=T; //p是遍历指针
while(p||!IsEmpty(S)){ //栈不空或p不空时循环
if(p){ //一路向左
visit(p); //访问当前结点
Push(S,p); //入栈
p=p->lchild; //左孩子不空,一直向左走
}
else{ //出栈,并转向出栈结点的右子树,可改成if(!IsEmpty(S))
Pop(S,p); //栈顶元素出栈
p=p->rchild; //向右子树走,p赋值为当前结点的右孩子
} // 返回while循环继续进入if-else语句
}
}
中序遍历(\(LNR\))
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
递归代码如下:
void InOrder(BiTree T)
{
//先序遍历算法
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
vist(T); //访问根结点,如:printf("%c",T->data);
InOrder(T->rchild); ////递归遍历右子树
}
}
非递归代码如下:
void Inorder2(BiTree T){
//中序遍历非递归算法
InitStack(S);//需要借助一个递归栈
BiTree p=T;
while(p||!IsEmpty(S)){ //栈不空或者P不空时循环
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
visit(p);
p=p->rchild;
}
}
}
后序遍历(\(LRN\))
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
递归代码如下:
void PostOrder(BiTree T)
{
//先序遍历算法
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); ////递归遍历右子树
vist(T); //访问根结点,如:printf("%c",T->data);
}
}
非递归代码如下(重难点!!!):
void PostOrder(BiTree T)){
InitStack(S);
BiTree p=T; //工作指针
r=NULL;//指向最近访问过的结点,辅助指针
while(p||!IsEmpty(S)){
if(p){
//1、从根结点到最左下角的左子树都入栈
Push(S,p);
p=p->lchild;
}
else{ //返回栈顶的两种情况
GetTop(S,P);//弹出栈顶元素
if(p->rchild&&p->rchild!=r){
//1、右子树存在且未访问过,
p=p->rchild;//转右
push(S,p); //压入栈
p=p->lchild;//走到最左
}
else{
//2、右子树已经访问或空,接下来出栈访问结点
pop(S,p); //将结点弹出
visit(p->data); //访问该结点
r=p; //指针访问过的右子树根结点
p=NULL;//访问完之后就重置P,每次从栈中弹出一个,防止进入第一个if
}
}
}
}
难点:要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点