目录
1.知识总览
2.二叉树的基本概念
(1).满二叉树
(2).完全二叉树
(3).二叉排序树
(4).平衡二叉树
3.二叉树常考点
i.叶子结点与二分支结点的数量关系
ii.第i 层的最多结点数(i >= 1)
iii.高度为h 的二叉树的最多结点数
iiii.完全二叉树:已知有 n 个结点,求高度
iiiii.完全二叉树:已知有 n 个结点,求度为0、1、2的结点个数(n0、n1、n2)
4.二叉树的存储结构
(1).顺序存储
(2).链式存储
5.二叉树的遍历
(1).前/中/后序遍历
i.代码
(2).层次遍历
i.代码
(3).由序列推出二叉树原图
6.线索二叉树
i.手算
ii.机算(二叉树的线索化)
7.线索二叉树的后继、前驱检索
· 中序(左 根 右)
(1).找后继
(2).找前驱
· 先序(根 左 右)
(1).找后继
(2).找前驱
· 后序(左 右 根)
(1).找后继
(2).找前驱
1.知识总览
2.二叉树的基本概念
二叉树是n个结点的有限集合:
· n == 0时,为空二叉树。
· n != 0时,由一个根节点和两个互不相交的被称为根的左右子树组成, 左右子树又分别时一棵二叉树。
特点:
· 每个结点至多有两棵子树。
· 左右子树不可颠倒位置,即二叉树是有序树。(注:与度为2的有序树做区分)
(1).满二叉树
· 高度为h,则含有2^h-1个结点。
· 叶子结点只存在于最后一层。
· 所有结点的度,要么为2,要么为0.
· 按层序从1开始编号,结点i 的左孩子为2i,右孩子为2i+1,父节点为 i/2向下取整(如果存在)
(2).完全二叉树
当前树(未必是满的)的编号与对应的满二叉树的编号一一对应。
假如当前二叉树与上图中的满二叉树对应,但缺少13号结点,按一般编码习惯,14号的名字要修改为13号,15号改为14号,此时,就不能被称为是完全二叉树,因为编号不对应了。完全二叉树,若缺少结点,则只可从最后的结点开始,按序缺失,保证编码统一。
定义完全二叉树除了最后一层外,其他的每一层都会被填满,即从下往上,按序号由大到小删除,所以,上图的7号结点不能被直接删除,但14、15可以被删除,此时7号结点也成了叶子结点。
特点·总结:
· 叶子结点只会出现在最后两层。
· 最多只能有一个度为1的结点。
· 按层序从1开始编号,结点i 的左孩子为2i,右孩子为2i+1,父节点为 i/2向下取整(如果存在)(与满二叉树相同)
(3).二叉排序树
左子树永远小于根节点。
右子树永远大于根节点。
(其中的子树也遵循以上原则)
常用于元素的排序与搜索。
(4).平衡二叉树
树上任意结点的左右子树的深度之差不超过1.
具有更高效的搜索效率。
3.二叉树常考点
i.叶子结点与二分支结点的数量关系
非空二叉树中度为0、1、2 的结点数分别有n0、n1、n2,则有
n0 = n2 + 1 //叶子结点数 = 二分支结点数 + 1
ii.第i 层的最多结点数(i >= 1)
在上一章对一般树的介绍中,就有提到该类问题的公式,m^(i-1).
二叉树第i 层至多有 2^(i-1) 个结点
iii.高度为h 的二叉树的最多结点数
当结点最多时,此二叉树就是满二叉树,结点数为 2^h - 1个。
iiii.完全二叉树:已知有 n 个结点,求高度
iiiii.完全二叉树:已知有 n 个结点,求度为0、1、2的结点个数(n0、n1、n2)
4.二叉树的存储结构
(1).顺序存储
#define MaxSize 100
//顺序存储
class TreeNode
{
public:
int data; //储存数据
bool isEmpty; //标记是否为空 //初始化是需要遍历各结点,将之设为true
};
TreeNode t[MaxSize]; //定义一个长度为MaxSize的数组t,依据二叉树从上至下、从左至右的顺序依次存放数据
定义一个长度为MaxSize的数组t,依据二叉树从上至下、从左至右的顺序依次存放数据。
一般,数组的t[0] 是废弃不用的,以保证结点编号于数组编号一致。
注:如果二叉树的形式不是完全二叉树,那么需要我们人为的将结点的编号转化为对应的完全二叉树的编号,再储存在于编号对应的数组位置。
在非完全二叉树中,判断是否有左孩子之类的问题,无法只依靠完全二叉树的公式来计算,需要对通过公式得到的结点,进行进一步的判空操作(isEmpty)。
对于顺序存储,适合存完全二叉树,非完全的存储会浪费大量的空间。实际上,很少用顺序存储来储存二叉树。
(2).链式存储
//链式存储
class BiTNode
{
public:
int data; //数据域
BiTNode* lchild, * rchild; //指针域,左右孩子指针
//BiTNode* parent; //如果该二叉树需要频繁获取父节点,则可再引入一个父节点的指针
};
using BiTree = BiTNode*;
设一个二叉树有n个结点,则有2n个指针域,易知,会有n - 1个结点头上有一个指针指向它,所以,会存在n+1(2n-(n-1))个指针置空。 (这些空指针会用来构造线索二叉树)
5.二叉树的遍历
(1).前/中/后序遍历
设定一个二叉树(含有一个根节点、一个左子树、一个右子树),其结点间的遍历顺序如下,
前序:根左右。(或称,先序)
中序:左根右。
后序:左右根。
对于上图的各结点访问循序,
前序:A BDE CFG
中序:DBE A FCG
后序:DEB FGC A
在用二叉树对算数表达式的分析中,先序——前缀表达式,中序——中缀表达式(需要认人为的添加部分括号),后序——后缀表达式
i.代码
这三序遍历的代码都很简单,都是递归
void visit(BiTNode* T)
{
cout << T->data << endl;
}
//前序
void PreOrder(BiTree T)
{
if (T != nullptr)
{
visit(T); //访问根节点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
//中序
void InOrder(BiTree T)
{
if (T != nullptr)
{
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
//后序
void PostOrder(BiTree T)
{
if (T != nullptr)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
(2).层次遍历
· 初始化一个辅助队列。
· 先将根节点入队。
· 若队列非空,则队头出队,并访问出队结点,同时,将其左右孩子(与该结点直接相连的两个结点)插入队列(此时,无需因非空,而发生再次的队头出队)。
· 再次判断队列是否为空,重复第三步操作,直到队列为空为止。
i.代码
//链式存储
class BiTNode
{
public:
int data; //数据域
BiTNode* lchild, * rchild; //指针域,左右孩子指针
};
using BiTree = BiTNode*;
//访问结点
void visit(BiTree T)
{
cout << T->data << endl;
}
//链式队列结点
class LinkNode
{
public:
BiTNode* data;
LinkNode* next;
};
//链式队列
class LinkQueue
{
public:
LinkNode* front, * rear;
};
//初始化(带头结点)
void InitQueue(LinkQueue& Q)
{
Q.front = Q.rear = new LinkNode();
Q.front->next = nullptr;
}
//判空(带头结点)
bool QueueEmpty(LinkQueue& Q)
{
if (Q.front == Q.rear)
return true;
return false;
}
//入队(带头结点)(只能尾插)
bool EnQueue(LinkQueue& Q, BiTree x)
{
LinkNode* p = new LinkNode();
p->data = x;
p->next = nullptr;
Q.rear->next = p;
Q.rear = p;
return true;
}
//出队(带头结点)
bool DeQueue(LinkQueue& Q, BiTNode* x)
{
if (Q.front == Q.rear)
return false;
LinkNode* p = Q.front->next; //设置一个临时结点,指向队头
x = p->data;
Q.front->next = p->next; //绕开队头结点
if (Q.rear == p) //如果队头结点就是队尾结点,那么还需要更新rear 指针
Q.rear = Q.front;
free(p);
return true;
}
//链式队列不会有队满情况
//打印全队(带头结点)
bool PrintLQueue(LinkQueue Q)
{
if (QueueEmpty(Q))
return false;
LinkNode* p = Q.front->next;
int i = 1;
while (1)
{
cout << "第" << i << "个元素:" << p->data << endl;
if (p == Q.rear)
break;
p = p->next;
i++;
}
}
//层次遍历
void LevelOrder(BiTree T)
{
LinkQueue Q;
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q, T); //根节点入队
while (!QueueEmpty(Q)) //队列不空,就循环
{
DeQueue(Q, p); //队头结点出队
visit(p); //访问队头结点
if (p->lchild != nullptr)
EnQueue(Q, p->lchild); //左孩子入队
if (p->rchild != nullptr)
EnQueue(Q, p->rchild); //右孩子入队
}
}
(3).由序列推出二叉树原图
单一序列(前中后序,层序),可以堆出多种二叉树,但两种序列(其中必须有一个是中序序列)作用在一起,就可以推出唯一的序列。
推到的核心思路,根据前或后或层序,先判断出根节点,再根据中序得到左右子树,。
6.线索二叉树
线索二叉树,其核心作用就是寻找一个结点的在序列中的前驱、后继。
在一个正常二叉树中,其叶子结点的左右孩子指针必然是置空的,而线索二叉树就是利用这些置空的指针创建的。
i.手算
构造思路:
首先,对二叉树进行排序(前序、中序、后序)。
然后,根据序列生成双向链表。
如此,就得到了相应序列的线索二叉树。(生成的链表叫线索链表)
存储结构:
在二叉树中,有些结点其左右孩子不是置空的,其左右孩子指针无法利用,需要进行标记。
一般引入两个tag,标记左右指针是否指示线索,tag == 0 指向左右孩子;tag == 1 指向线索。
//线索二叉树
class ThreadNode
{
public:
int data; //数据域
ThreadNode* lchild, * rchild; //指针域,左右孩子指针
int ltag, rtag; //标记左右指针是否指示线索,初始化时要进行处理
};
using ThreadTree = ThreadNode*;
ii.机算(二叉树的线索化)
普通方法寻找中序二叉树前驱:
//二叉树
class BiTNode
{
public:
int data; //数据域
BiTNode* lchild, * rchild; //指针域,左右孩子指针
};
using BiTree = BiTNode*;
//辅助全局变量
BiTNode* p; //指向指定的结点
BiTNode* pre = nullptr; //指向当前访问结点的前驱
BiTNode* final = nullptr; //指向最终需要的前驱结点
//访问结点
void visit(BiTNode* q)
{
if (q == p) //当访问节点为目标结点时
final = pre; //直接获得前驱
else
pre = q; //否则,将前驱移动到下一位
}
//中序
void InOrder(BiTree T)
{
if (T != nullptr)
{
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
中序线索化:
//线索二叉树
class ThreadNode
{
public:
int data; //数据域
ThreadNode* lchild, * rchild; //指针域,左右孩子指针
int ltag, rtag; //标记左右孩子是否存在,初始化时要进行处理
};
using ThreadTree = ThreadNode*;
ThreadNode* pre = nullptr; //指向当前访问结点的前驱
//访问结点
void visitThread(ThreadNode* q)
{
if (q->lchild == nullptr) //左子树为空,建立前驱线索
{
q->lchild = pre;
q->ltag = 1;
}
if (pre != nullptr && pre->rchild == nullptr) //建立前驱结点的后继
{
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
//中序遍历
void InThread(ThreadTree T)
{
if (T != nullptr)
{
InThread(T->lchild);
visitThread(T);
InThread(T->rchild);
}
}
//中序线索化
void CreatInThread(ThreadTree T)
{
pre = nullptr; //pre初始化为null
if (T != nullptr) //非空二叉树才能线索化
{
InThread(T); //中序线索化二叉树
if (pre->rchild == nullptr)
{
pre->rtag = 1; //最后一个结点的rtag 要设置为1
}
}
}
注:在先序线索化中,需要对先序遍历进行一点处理,如下
//先序遍历
void InThread(ThreadTree T)
{
if (T != nullptr)
{
visitThread(T);
//需要确保左孩子不是线索,因为visit之后,当前节点的左孩子可能指向其父节点,若无该判断可能会出现循环
if(T->ltag == 0)
InThread(T->lchild);
InThread(T->rchild);
}
}
7.线索二叉树的后继、前驱检索
在下文的前驱、后继的搜索中,会由于序列排序的特性,使得在遍历中需要用到三叉树(或是土方法,全部遍历一次找到所需节点)。
检索时,默认p 作为子树根节点,观察p 为首的子树以及其兄弟子树。
· 中序(左 根 右)
(1).找后继
以中序为例,对于一个结点,找其后继,
· 先看右孩子,若右孩子为线索(rtag == 1) ,则可直接根据线索找到后继;若为结点(rtag == 0),考虑中序排列的特性左 根 (左 根 右),很显然,其后继就是右子树的左分支中最左下角的结点。
· 代码如下,
//寻找以p 为根的子树中,第一个被中序遍历的结点(即,左分支最左下角的结点)
ThreadNode* Firstnode(ThreadNode* p)
{
while (p->ltag == 0) //一直为0,说明一直有左孩子结点,故一直遍历下去
p = p->lchild;
return p;
}
//找到中序线索二叉树p结点的后继
ThreadNode* Nextnode(ThreadNode* p)
{
if (p->rtag == 0)
return Firstnode(p->rchild);
else
return p->rchild; //tag为1,说明含有线索,直接根据线索就可以找到后继
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode* T)
{
for (ThreadNode* p = Firstnode(T); p != nullptr; p = Nextnode(T))
visitThread(p);
}
(2).找前驱
与上文中,找后继的原理相同,
以中序为例,找前驱,(左 中 右)中 右,即找左孩子的最右下角的节点。
代码如下,
//获取p子树的最右下角节点
ThreadNode* Lastnode(ThreadNode* p)
{
while (p->ltag == 0) //一直为0,说明一直有右孩子结点,故一直遍历下去
p = p->rchild;
return p;
}
//找到中序线索二叉树p结点的前驱
ThreadNode* Prenode(ThreadNode* p)
{
if (p->ltag == 0)
return Lastnode(p->lchild);
else
return p->lchild; //tag为1,说明含有线索,直接根据线索就可以找到后继
}
//对中序线索二叉树进行逆向中序遍历(利用线索实现的非递归算法)
void RevInorder(ThreadNode* T)
{
for (ThreadNode* p = Lastnode(T); p != nullptr; p = Prenode(T))
visitThread(p);
}
· 先序(根 左 右)
(1).找后继
对于先序,根 左 右,在找后继中,都将p 节点当作序列中的根(rtag标记后继线索)
· rtag == 1(右孩子指针被线索化)(没有右孩子),next = p->rchild
· rtag == 0(右孩子存在),
假设有左孩子(根(根 左 右)右),next = p->lchild(后继就是左子树的根节点)
假设没有左孩子(根(根 左 右)),next = p->rchild(后继就是右子树的根节点)
(2).找前驱
找前驱需要知道对应节点的父节点,而获取父节点的操作一般是写一个遍历整个树获取对应节点父节点的函数,也可以将二叉树改为一个含有父节点指针的三叉树。
· p->ltag == 1,则 preNode = p->lchild
· p->ltag == 0,
首先,要找到它的父节点,
i.p 为其父节点的左孩子(pre->rchild==p),则p 的前驱就是其父节点。
ii.p 为其右孩子,且p 的左兄弟为空,则p 的前驱就是其父节点。
iii.p 为其右孩子,且p 的左兄弟非空,则p 的前驱就复杂了,是p 的左兄弟最后遍历的节点。
寻找思路:
从其父节点的左孩子出发(以下是在该左子树中进行的操作),直接一直向右寻找直到可以直接找到的最后一个右孩子(part1)(while:q = q->rchild),判别这个右子树是否有左孩子(part2)(if:q->lchild != nullptr),如果有,则向左找,向左移动一位(q = q->lchild),再判别该节点是否有右孩子,若有,则向右持续移动,若没有,判断是否有左孩子,即重复上文part2往后的操作,直到遍历到叶子节点,此时的这个节点就是p 的前驱。如果没有,则该节点就是左子树先序遍历的最后一个节点。
总之,就是能往右走,就往右走,走不通,再看看是否能左走,以及左走之后,还能再右走不。
iiii.p 为这个树的根节点,则没有前驱。
· 后序(左 右 根)
(1).找后继
· rtag == 1,则next = p->rchild
· rtag == 0(右孩子存在),
由于p 只能向下找孩子,在后序序列中,根节点居于末尾,后继难以查询,需要引入三叉树或用土方法获得其父节点。
左 右 根
> (左 右 根) (左 右 根) 根
i.p 作为父节点的右孩子,其后继就是其父节点。
ii.p 作为父节点的左孩子,其后继又麻烦了,需要观察兄弟节点,如果没有兄弟节点,那后继就是父节点;如果有,后继就是兄弟节点在兄弟子树中,后序遍历最先访问的节点(最左),其内的逻辑与上文那些,不能说完全相同吧,也就是一摸一样,到最左末端,看看这个末端是否有右孩子,有就往右一个,再看看有左有右,直到遇到叶子节点。
iii.p 作为整个树的根节点,则没有后继。
(2).找前驱
· p->ltag == 1,则pre = p->lchild
· p->ltag == 0,
此时,p 必有左孩子,根据后继序列的特性,可以很轻易的知道,当p 的右孩子存在时,其前驱就是它的右孩子,右孩子不存在时,前驱就是左孩子。
左 右 根
> (左 右 根) (左 右 根) 根