二、遍历
2.4 迭代后序遍历
解决办法:
- 找到第一个被访问的节点
- 将沿途各节点的祖先及右兄弟(若存在)用栈保存
从根出发下行,尽可能沿左分支,实不得已,才沿右分支。最后一个节点必是叶子,也是递归版中visit()首次执行处。这叶子将首先接受访问
第一个被访问的节点
从左侧水平向右看去,未被遮挡的最高叶节点v——称作最高左侧可见叶节点(HLVFL)——即为后序遍历首先访问的节点。请注意,该节点既可能是左孩子,也可能是右孩子,
图摘自清华大学《数据结构(C++语言版)》
后序遍历的过程也可分为模式雷同的若干段:
- 访问当前节点
- 遍历以其右兄弟(若存在)为根的子树
- 向上回溯至其父节点(若存在)并转入下一片段。
代码摘自清华大学《数据结构(C++语言版)》
template <typename T> //在以S栈顶节点为根的子树中,找到最高左侧可见叶节点
static void gotoLeftmostLeaf(Stack<BinNodePosi(T)>& S) { //沿途所遇节点依次入栈
while (BinNodePosi(T) x = S.top()) //自顶而下,反复检查当前节点(即栈顶)
if (HasLChild(*x)) //尽可能向左
{
if (HasRChild(*x))
S.push(x->rc); //若有右孩子,优先入栈
S.push(x->lc); //然后才转至左孩子
}
else //实不得已
S.push(x->rc); //才向右
S.pop(); //返回之前,弹出栈顶的空节点
}
template <typename T, typename VST>
void travPost_I(BinNodePosi(T) x, VST& visit) { //二叉树的后序遍历(迭代版)
Stack<BinNodePosi(T)> S; //辅助栈
if (x) S.push(x); //根节点入栈
while (!S.empty()) //x始终为当前节点
{
if (S.top() != x->parent) 若栈顶非x之父(而为右兄)
gotoLeftmostLeaf(S); //则在其右兄子树中找到HLVFL(相当于递归深入)
x = S.pop(); visit(x->data); //弹出栈顶(即前一节点之后继),并访问之
}
}
辅助函数:gotoLeftmostLeaf(),自顶而下反复检查栈顶节点,尽可能地向左深入,若有右孩子,则右孩子优先入栈,左孩子后入栈(这两个孩子就是兄弟关系,原节点为二者的父节点),然后转向左孩子;实不得已(没有左孩子),才转向右孩子。由于这里的循环迭代以栈顶为空结束,所以辅助函数返回之前,要弹出栈顶的空节点
主算法:建立辅助栈。根节点首先入栈(注意前面两种算法没有此步骤)。若栈顶节点并非当前访问节点之父(而为右兄,因为从入栈的过程来看,只有两种可能),则调用gotoLeftmostLeaf()在其右兄子树中,找到最靠左的叶子HLVFL(相当于递归深入其中)。然后弹出当前栈顶(即前一节点的后继),并访问之。
算法终止条件:辅助栈为空(注意后序遍历的算法终止条件是在while的入口条件中,而不像前面两种遍历是在调用完辅助函数后判断栈是否为空,为空再break,而while(true))
每个节点出栈后,以之为根的子树已经完全遍历,且其右兄弟r若存在,则必在栈顶(因为栈顶只可能是其兄或其父)。此时正可以从r出发开始遍历子树r
2.5 层次遍历
2.5.1 算法实现
此前的三种遍历,都不能保证所有的节点严格地按照深度次序接受访问,即存在逆序,需要借助栈Stack。
在层次遍历中,所有节点都严格按照深度次序,由高至低地接受访问,同样深度的节点由左至右访问。由于严格按照次序,因此借助队列Queue(先入先出FIFO)
图截取自清华大学数据结构慕课
- 引入辅助队列
- 根节点入队
- 每次取出队首节点,并访问
- 若有左孩子,则左孩子入队
- 若有右孩子,则右孩子入队
- 反复循环3~5步
算法终止条件:队列变空
注意:此处4、5步的顺序是先左孩子入队,再右孩子入队,是顺序的,而不像先序遍历的版本1那样是逆序的(因为层次遍历用队列,FIFO,先序遍历用栈,LIFO)
出于栈的这种特性,任意时刻队列中的节点满足深度相差不超过1
代码摘自清华大学《数据结构(C++语言版)》
/*DSA*/#include "queue/queue.h" //引入队列
template <typename T> template <typename VST> //元素类型、操作器
void BinNode<T>::travLevel(VST& visit) //二叉树层次遍历算法
{
Queue<BinNodePosi(T)> Q; //辅助队列
Q.enqueue(this); //根节点入队
while (!Q.empty()) //在队列再次变空之前,反复迭代
{
BinNodePosi(T) x = Q.dequeue(); visit(x->data); //取出队首节点并访问之
if (HasLChild(*x)) Q.enqueue(x->lc); //左孩子入队
if (HasRChild(*x)) Q.enqueue(x->rc); //右孩子入队
}
}
每次迭代,都恰好有一个节点出队并接受访问,同时有不超过两个节点入队。每个节点入、出队恰好各一次,故整体只需O(n)时间
2.5.2 完全二叉树
若在对某棵二叉树的层次遍历过程中,前n/2次迭代中都有左孩子入队,且前n/2 - 1次迭代中都有右孩子入队,则称之为完全二叉树(complete binary tree)。
完全二叉树的结构特征:叶节点只能出现在最底部的两层,且最底层叶节点均处于次底层叶节点的左侧。
图摘自清华大学《数据结构(C++语言版)》
由图,高度为h的完全二叉树,规模n应该介于 2 h 2^h 2h至 2 h + 1 − 1 2^{h+1}-1 2h+1−1之间;反之,规模为n的完全二叉树, h = O ( l o g n ) h=O(logn) h=O(logn)。
高度为h的完全二叉树节点最多的情况即满二叉树,最少的情况比高度为h-1的满二叉树多1。
2.5.3 满二叉树
每一层的节点数都应达到饱和,故将称其为满二叉树(full binary tree)。满二叉树所有叶节点同处于最底层(非底层节点均为内部节点)。
高度为h的满二叉树由
2
h
+
1
−
1
2^{h+1}-1
2h+1−1个节点组成,其中叶节点总是恰好比内部节点多出一个(叶节点树
2
h
2^h
2h,内部节点数
2
h
−
1
2^h-1
2h−1)
图摘自清华大学《数据结构(C++语言版)》
2.6 重构
已知某棵数的遍历序列,还原出该树的拓扑结构
情况1:[ 先序 | 后序 ] + 中序
情况2:[ 先序 | 后序 ] + 真(真二叉树)
题
1、并查集中的树最适合用什么方法表示:
父节点法能够高效定位父亲而不能高效地定位孩子,而并查集中的树只需要能够定位父节点。
2、每上溯一层,深度减小1,但高度的增加可能大于1,因为节点的高度由其左、右子树中较高者决定。
3、借助队列对二叉树进行层次遍历时,任意时刻队列中的节点满足:深度相差不超过1