概述
二叉树的遍历,就是按照一定的规则访问二叉树,
将二叉树的非线性结构转换为二叉树结点的一个线性序列
假设 L, R, V 分别代表遍历一个结点的左子树,右子树,以及访问该结点的操作
则遍历总共有六种规则:
VLR
VRL
------ 前序
LVR
RVL
------ 中序
LRV
RLV
------ 后序
为了方便,以下算法都采用先左后右的三种,先右后左同理
三种遍历的递归算法
当树非空时:按照顺序访问,若是L或者R则递归访问,遇到V就直接访问.
中序遍历的递归算法:
template <class T>
void BinaryTree<T>::inOrder(BinTreeNode<T> *subTree, void(*visit)(BinTreeNode<T> *p))
{
if (subTree != NULL)
{
InOrder(subTree->leftChild, visit);
visit(subTree);
InOrder(subTree->rightChild, visit);
}
}
前序遍历的递归算法:
template <class T>
void BinaryTree<T>::PreOrder(BinTreeNode<T> *subTree, void(*visit)(BinTreeNode<T> *p))
{
if (subTree != NULL)
{
visit(subTree);
PreOrder(subTree->leftChild, visit);
PreOrder(subTree->rightChild, visit);
}
}
后序遍历的递归算法:
template <class T>
void BinaryTree<T>::PostOrder(BinTreeNode<T> *subTree, void(*visit)(BinTreeNode<T> *p))
{
if (subTree != NULL)
{
PostOrder(subTree->leftChild, visit);
PostOrder(subTree->rightChild, visit);
visit(subTree);
}
}
为了把一个递归过程改写为一个非递归过程需要利用一个工作栈,记录遍历时的回退路径
前序遍历的非递归算法
前序遍历时,当前结点的访问时机就是当前,
所以只需要在访问左或者右的时候用栈暂存另一边即可
第一种
初始化:p取根结点,栈变为空
循环:
访问结点
预留右子树指针在栈中
左子树非空则进入左子树,左子树为空则弹出右子树,下一次循环访问的就是右子树
template <class T>
void BinaryTree<T>::preOrder(void(visit)(BinTreeNode<T> *p))
{
stack<BinTreeNode<T>*> S;
BinTreeNode<T> *p = root;
S.Push(NULL);
while (p != NULL)
{
visit(p); //访问当前结点
if (p->rightChild != NULL)
S.Push(p->rightChild); //右子树不为空就将右子树进栈
if (p->leftChild != NULL) //左子树不为空下一次循环就访问左子树根结点
p = p->leftChild;
else
S.Pop(p); //左子树为空就释放刚刚保存或者前面保存的右子树,下一次访问
}
}
另外一种方法:
初始化:根结点入栈
循环:
p取栈顶元素,访问p
如果右子树存在,右子树进栈
如果左子树存在,左子树进栈
template <class T>
void BinaryTree<T>::PreOrder(void(visit)(BinTreeNode<T> *p))
{
stack<BinTreeNode<T>*> S;
BinTreeNode<T> *p;
S.Push(root);
while (p != NULL)
{
S.Pop(p);
visit(p);
if (p->rightChild != NULL)
S.Push(p->rightChild);
if (p->leftChild != NULL)
S.Push(p->leftChild);
}
}
中序遍历的非递归算法
中序遍历时,当前结点的访问时机是左孩子为空或者左孩子访问完了
所以栈用来存放当前结点,当左孩子为空时就是访问的时机,
算法描述:
初始化:栈为空,p取根结点
重复以下操作
当p不为空时,p进栈,p指向其左孩子,直到p左为空
若栈非空,出栈一个,访问,p指向其右孩子
直到p为空,且栈为空(do-while结构)
template <class T>
void BinaryTree<T>::InOrder(void(*visit)(BinTreeNode<T> *p))
{
stack<BinTreeNode<T>*> S;
BinTreeNode<T> *p = root;
do
{
//左孩子不停入栈,直到左孩子为空
while (p != NULL)
{
S.Push(p);
p = p->leftChild;
}
//执行到此处,说明前一个p的左孩子为空
//所以此时应该访问前一个p的父结点,即栈中最后进入的元素
if (!S.IsEmpty())
{
S.Pop(p);
visit(p); //执行到此处,左孩子和父结点都访问完了,所以应该考察右孩子
p = p->rightChild;
}
} while (p != NULL || !S.IsEmpty());
}
结束条件为栈为空且遍历指针为空。
栈不为空,遍历指针为空表示左子树访问完了,该访问栈里面的结点了
栈为空,但指针不为空时表示右子树还没访问。
后序遍历的非递归算法
后序遍历的一个重要性质:当前访问节点的时候,工作栈内的元素顺序刚好是其祖先结点到它的路径
后序遍历的非递归比前序和中序复杂。
在遍历左子树时不能访问根结点,还要遍历右子树。
右子树遍历完了才能访问根结点。
所以我们要搞清楚的是上次访问时在左子树中还是右子树中。
方法一(结点中增加标志域):
即每次入栈时,要同时给进栈结点一个标记,在访问完左子树时,还要把栈顶结点的标记改为右。
经过分析,访问结点的时机就是其右孩子为空或者右孩子访问完了的时候
总结算法步骤:
初始化:栈为空,p取根结点
重复以下操作:
当p不为空时,进栈,标记为左,直到左孩子为空
定义一个标记,用于判断是否处于左子树,赋值真
当处于左子树且栈非空时,
取出栈顶元素,
判断标记:
如果是左标记,将其改为右,再放进去,左子树标记改为假,并指向其右孩子
如果是右标记,访问
直到栈为空
template <class T>
void BinaryTree<T>::PostOrder(void(*visit)(BinTreeNode<T> *p))
{
Stack<stkNode<T>> S;
stkNode<T> w;
BinTreeNode<T> *p = root;
do
{
while (p != NULL)
{
w.ptr = p;
w.tag = L;
S.Push(w);
p = p->leftChild;
}
int continue1 = 1;
while (continue1 %% !S.IsEmpty())
{
S.Pop(w);
p = w.ptr;
switch (w.tag)
{
case L:
w.tag = R;
S.Push(w);
continue1 = 0;
p = p->rightChild;
break;
case R:
visit(p);
break;
}
}
} while (!S.IsEmpty());
cout << endl;
}
方法二(增加辅助指针,记录最后一次访问的结点):
当 p非空或栈非空时:
如果p非空,则p的左孩子进栈
否则:
p取栈顶元素
如果p右孩子存在且上次没被访问,p指向右孩子
否则:
出栈
访问p元素
记录访问的结点
p置空
另一种写法:
p取根结点,last取root,根结点进栈
当栈非空时:
p取栈顶元素
如果,左右孩子都存在或者右孩子为空last为左孩子或者last为右孩子
访问p指向结点,last设为p,s出栈
否则:
如果右孩子存在,右孩子进栈
如果左孩子存在,左孩子进栈
方法三:
p取根结点,栈s为空
p进栈两次
当栈非空时:
p取栈顶元素,s出栈
如果栈非空且p等于栈顶元素
若p右孩子存在,右孩子进栈两次
若p左孩子存在,左孩子进栈两次
否则:
访问p指向的结点
二叉树的层次序遍历
入队顺序即出队顺序,也就是访问顺序
入队时一定要,先上后下,先左后右
初始化:p取根结点,根结点入队列
当队列非空时:
出队列,访问出来的元素
如果出来的元素有左孩子,则其左孩子入队列
如果出来的元素有右孩子,则其右孩子入队列
template <class T>
void BinaryTree<T>::levelOrder(void(*visit)(BinTreeNode<T> *p))
{
Queue<BInTreeNode<T>*> Q;
BinTreeNode<T> *p = root;
Q.EnQueue(p);
while (!Q.IsEmpty())
{
if (p->leftChild != NULL)
Q.EnQueue(p->leftChild);
if (p->rightChild != NULL)
Q.EnQueue(p->rightChild);
}
}