文章目录
知识框架:
![知识框架](https://i-blog.csdnimg.cn/blog_migrate/be6303941552f96dd8b98409e5ec1ca0.png)
树的性质:
- 树的结点数等于所有结点的度数加1。
- 度为m的树中第i层上至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i \geq 1 i≥1)。
- 高度为h的m叉树之多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m−1mh−1个结点。
- 具有n个结点的m叉树的最小高度为
l
o
g
m
(
n
(
m
−
1
)
+
1
)
log_m(n(m-1)+1)
logm(n(m−1)+1)
性质的解释:
- 首先什么是度数:一个结点的子结点个数称为该结点的度,树中结点的最大度数称之为树的度,故:结点的度数之和=子结点数+1(根节点没有父母结点);
- 度为m,所以每一个结点的最大子结点数为m,每一层的结点数是一个等比数列: a 1 = 1 , a n = m i − 1 a_1=1,a_n=m^{i-1} a1=1,an=mi−1;
- 假设每层的节点数都是最多的,而每一层的结点数是一个等比数列: a 1 = 1 , a n = m n − 1 a_1=1,a_n=m^{n-1} a1=1,an=mn−1,求该等比数列的前n项和: S n = m n − 1 m − 1 S_n=\frac{m^{n}-1}{m-1} Sn=m−1mn−1;
- 已知等比数列的前n项和为 S n S_n Sn,求n: s = l o g m ( S n ( m − 1 ) + 1 ) s=log_m({S_n}{(m-1)}+1) s=logm(Sn(m−1)+1);
二叉树
二叉树的分类
二叉树:度为2的树;
分类:
- 满二叉树:二叉树插满结点;
特征:- 编号后,编号为i的结点的父母结点编号为:
[
i
2
]
[\frac{i}{2}]
[2i],左孩子结点编号为:
2
i
2i
2i;右孩子结点的编号为:
2
i
+
1
2i+1
2i+1;
解释:编号后,编号为i的结点在树中的位置可以确定且唯一,所以可以确定其父母结点及孩子结点的位置及编号,证明从略;
- 编号后,编号为i的结点的父母结点编号为:
[
i
2
]
[\frac{i}{2}]
[2i],左孩子结点编号为:
2
i
2i
2i;右孩子结点的编号为:
2
i
+
1
2i+1
2i+1;
- 完全二叉树:从叶子结点开始倒着缺失的满二叉树;
特征:- 1.如果
i
≤
n
2
i \leq \frac{n}{2}
i≤2n,则结点i为分支结点,否则为叶子结点;
解释:最后一个叶子结点的编号为n,则该叶子结点的父母结点为最后一个分支结点,编号为 [ n 2 ] [\frac{n}{2}] [2n]; - 2.叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上;(基本是废话)
- 如果有度为1的结点,只可能有一个,且该结点只有左孩子没有右孩子(重要特征?)
解释:编号最大的分支结点的可能情况之一;
- 按层编号后,一旦出现某结点(其编号为i)为叶子结点或者只有左孩子,则编号大于i的结点均为叶子结点。
解释:所有的叶子结点的特征:比该结点的编号大的结点都是叶子结点;最后一个分支结点的特征:要么只有左孩子结点跟其他分支结点区分开,要么左右孩子都有,跟其他分支节点区分不开,当然跳可以区分开的特征额外做特征说明啊。 - 若n为奇数,则每个分支结点都有左右孩子及结点;否则最后一个(编号最大)分支节点只有左孩子;
解释:树的结点数=子结点数+1(根节点),二叉树的每一层结点数的最大容量都是2的倍数,若总的n为奇数,最后一层为偶数,反之则为奇数;
- 1.如果
i
≤
n
2
i \leq \frac{n}{2}
i≤2n,则结点i为分支结点,否则为叶子结点;
- 二叉排序树:有如下性质的树:左子树上所有结点的关键字均小于根节点的关键字;右子树上的所有结点的关键字均大于根节点的关键字。左子树和右子树又各是一颗二叉排序树。
特征:- 所有的结点上按照中序排序法排序关键字,然后组成一棵树,或者说一棵树的关键字排序满足中序排序法;
- 平衡二叉树:树上的任一结点的左子树和右子树的深度之差不超过1,;
特征:- 树上的结点分布比较均匀,根节点到每一个叶子结点的距离比较均匀;
二叉树的性质(由于二叉树是特殊的树,输的性质二叉树都满足)
- 非空二叉树上叶子结点树等于度为2的节点数加1,即 N 0 = N 2 + 1 N_0=N_2+1 N0=N2+1,太简单,证明从略;
- 非空二叉树第K层上最多有 2 k − 1 2^{k-1} 2k−1个结点(树的性质);
- 高度为H的二叉树至多有 2 H − 1 2^{H}-1 2H−1个结点(树的性质);
- 对于完全二叉树,还是完全树的性质,数据更简单了,感觉没什么好说的。
- 具有N个(N>0)结点的完全二叉树的高度为 l o g 2 ( N + 1 ) 或 者 ( l o g 2 N + 1 ) log_2(N+1) 或者(log_2N+1) log2(N+1)或者(log2N+1);
二叉树的存储结构
- 顺序存储结构:按层遍历二叉树,然后将结点存储在一组地址连续的存储单元上;
适合类别:完全二叉树和满二叉树
优点:树中结点的序号可以唯一的反应出结点之间的逻辑关系,这样既能最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及节点之间的关系。
缺点:对于一般的二叉树,必须添加一些并不存在的结点让其每个节点与完全二叉树上的结点相对照,造成空间的浪费; - 链式存储结构:结构特点如下图所示:
存储结构:
结点代码:
class BiTNode{
public:
int data;
BiTNode *lchild,*rchild; //左右孩子指针
};
二叉树的遍历
递归遍历
- 先序遍历:根 +(左)+(右)
代码:
void PreOrder(BiTNode T){
if(T!=NULL){
visit(T);//访问根节点
PreOrder(T.lchild);//递归遍历左子树
PreOrder(T.rchild);//递归遍历右子树
}
}
时间复杂度:O(n)
空间复杂度:O(n)
- 中序遍历:(左)+ 根 + (右)
void InOrder(BiTNode T){
if(T!=NULL){
InOrder(T.lchild);//递归遍历左子树
visit(T);//访问根节点
InOrder(T.rchild);//递归遍历右子树
}
}
时间复杂度:O(n)
空间复杂度:O(n)
- 后序遍历:(左)+(右)+ 根
void PostOrder(BiTNode T){
if(T!=NULL){
PostOrder(T.lchild);//递归遍历左子树
PostOrder(T.rchild);//递归遍历右子树
visit(T);//访问根节点
}
}
时间复杂度:O(n)
空间复杂度:O(n)
- 层遍历(广度遍历):按层数从左到右遍历,完全二叉树编码
void LevelOrder(BiTNode T){
InitQueue(Q);//初始化辅助队列
BiTNode p;
EnQueue(Q,T);//根节点入队
while(!IsEmpty(Q)){
DeQueue(Q,p);//出队
visit(p);//访问当前结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);//左孩子节点入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);//右孩子节点入队
}
}
时间复杂度:O(n)
空间复杂度: O(n)
非递归遍历(实际上还是递归,只是用不是系统栈而是自己写的栈)
先序遍历
void PreOrderLoop(TreeNode *root)
{
std::stack<TreeNode *> s;
TreeNode *cur, *top;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
printf("%c ", cur->data);
s.push(cur);
cur = cur->left;
}
top = s.top();
s.pop();
cur = top->right;
}
}
原文链接:https://blog.csdn.net/Monster_ii/article/details/82115772
中序遍历
void PreOrderLoop(TreeNode *root)
{
std::stack<TreeNode *> s;
TreeNode *cur, *top;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
s.push(cur);
cur = cur->left;
}
top = s.top();
s.pop();
printf("%c ", cur->data);
cur = top->right;
}
}
原文链接:https://blog.csdn.net/Monster_ii/article/details/82115772
后序遍历
void PreOrderLoop(TreeNode *root)
{
std::stack<TreeNode *> s;
TreeNode *cur, *top, *last = NULL;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
s.push(cur);
cur = cur->left;
}
top = s.top();
if (top->right == NULL || top->right == last){
s.pop();
printf("%c ", top->data);
last = top;
}
else {
cur = top->right;
}
}
原文链接:https://blog.csdn.net/Monster_ii/article/details/82115772
这里面涉及到一个Catalan函数的问题,挺有意思的,建议学习一下:推导
这里有一个有趣的结论,一个数组的入栈序列等于相应二叉树的先序遍历序列、出栈序列等于对应二叉树的中序遍历序列,在非递归遍历代码中,我们可以发现一棵树中先序遍历与中序遍历的节点的入栈顺序是相同的,区别是一个在入栈时访问节点,一个是出栈时访问节点,所以对于一个数组,入栈顺序相同,出战顺序不同的组合,相当于先序遍历结果不变,中序遍历结果不同的组合有多少种,即节点数相同的二叉树有多少种。
相应的Catalan函数还有很多应用:应用
由遍历序列构造二叉树
思路:已知根节点,可以通过中序序列将其分为两棵树的中序序列,递归分割中序序列;
如何知根节点:先序序列第一个元素,后序序列最后一个元素,层遍历第一个元素;
- 方案1:先序序列+中序序列:
1.根节点(先序序列第一个元素)+中序序列=左子树中序序列+根节点+右子树中序序列;
2.左子树中序序列+先序序列=左子树先序序列;右子树中序序列+先序序列=右子树先序序列;
3.递归; - 方案2:后序序列+中序序列:
1.根节点(后序序列最后一个元素)+中序序列=左子树中序序列+根节点+右子树中序序列;
2.左子树中序序列+后序序列=左子树后序序列;右子树中序序列+后序序列=右子树后序序列;
3.递归; - 方案3:层序列+中序序列:
1.根节点(层序列第一个元素)+中序序列=左子树中序序列+根节点+右子树中序序列;
2.左子树中序序列+层序列=左子树层序列;右子树中序序列+层序列=右子树层序列;
3.递归;
线索二叉树
线索二叉树:在链表二叉树的基础上再加两个标志位,若无左子树,lchild指向前驱结点,标志位1变为1;若无右子树,rchild指向后继结点,标志位2变为1;
结点代码:
class ThreadNode{
public:
int data;
BiTNode *lchild,*rchild; //左右孩子指针
int ltag,rtag; //左右标志位
}
利用中序遍历对二叉树进行线索化:
void InThread(ThreadNode &p,ThreadNode &pre){
//中序遍历对二叉树线索化的算法
InThread(p->lchild,pre);//递归,线索化左子树
if (p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if (pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
InThread(p->rchild,pre);//递归,线索化右子树
}
相关代码分析:
1.求中序线索二叉树中中序序列下的第一个节点:(最左下结点)
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}
2.求中序线索二叉树中结点p在中序序列下的后继结点:
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag==0)
return Firstnode(p->rchild);//存在右子树,取右子树最左结点
else
return p->rchild;//不存在右子树,取线索二叉树的后继结点
}
中序遍历算法
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}