最近打算总结一下数据结构的知识,所以就决定写几篇文章记录一下好了。
树的遍历分为三种,先序遍历,中序遍历,后序遍历。而就我而言,了解以下三种实现遍历的方式:递归,利用栈,Morris遍历(比较酷炫的方法)。(本文使用java语言来实现)
那么就从最简单的递归实现开始吧。
//先序遍历
public void preorderRecursion(TreeNode root){
doSomething(root);
if(root.left!=null)
preorderRecursion(root.left);
if(root.right!=null)
preorderRecursion(root.right);
}
//中序遍历
public void inorderRecursion(TreeNode root){
if(root.left!=null)
inorderRecursion(root.left);
doSomething(root);
if(root.right!=null)
inorderRecursion(root.right);
}
//后序遍历
public void postorderRecursion(TreeNode root){
if(root.left!=null)
postorderRecursion(root.left);
if(root.right!=null)
postorderRecursion(root.right);
doSomething(root);
}
递归的实现是非常简单的,只需要修改一下doSomething()的顺序即可。
递归实现先序遍历的思想是首先将preorderRecursion看作实现树的先序遍历的函数,那么我们只需要
1. 对root节点doSomething
2. 对root的左子树进行先序遍历
3. 对root的右子树进行先序遍历
接下来是利用栈来实现遍历。
//先序遍历
public void preorderStack(TreeNode root){
Stack s = new Stack();
preorderStackSupport(root,s);
TreeNode temp;
while(!s.empty())
{
temp = (TreeNode)s.pop();
if(temp.right!=null)
preorderStackSupport(temp.right,s);
}
}
public void preorderStackSupport(TreeNode root,Stack s){
while(root.left!=null)
{
doSomething(root);
s.push(root);
root = root.left;
}
doSomething(root);
s.push(root);
}
辅助函数的作用是:将传入结点沿左儿子方向上所有的结点进行相应处理并入栈,这个过程保证了父结点->左儿子的顺序。
在调用了一次辅助函数之后,栈已经有了第一批按父节点->左儿子顺序处理过的元素。此时栈顶就是根结点的最左儿子。由于栈顶的左儿子为空,此时已经无法按父节点->左儿子的顺序进行下去,因此我们对栈顶的右儿子调用辅助函数。
//中序遍历
public void inorderStack(TreeNode root) {
Stack s = new Stack();
inorderStackSupport(root,s);
TreeNode temp;
while(!s.empty())
{
temp = (TreeNode)s.pop();
doSomething(temp);
if(temp.right!=null)
inorderStackSupport(temp.right,s);
}
}
public void inorderStackSupport(TreeNode root,Stack s){
while(root.left!=null)
{
s.push(root);
root = root.left;
}
s.push(root);
}
辅助函数的作用是:将传入结点沿左儿子方向上所有的结点入栈,根据栈的特性,我们知道入栈之后父节点会在儿子节点的下面。
在调用了一次辅助函数之后,栈已经有了第一批按父节点->左儿子顺序进入栈的元素,那么出栈时就是左儿子->父节点。此时栈顶就是根结点的最左儿子。与先序遍历不同,栈中的元素还未作相应处理。因此我们先对栈顶元素作相应处理,再对栈顶的右儿子调用辅助函数。
//后序遍历
public void postorderStack(TreeNode root){
Stack s = new Stack();
postorderStackSupport(root,s);
TreeNode temp,tempL;
while(!s.empty())
{
temp = (TreeNode)s.peek();
if(temp.right!=null)
postorderStackSupport(temp.right,s);
temp = (TreeNode)s.pop();
doSomething(temp);
tempL = (TreeNode)s.peek();
if(temp == tempL.right)
{
doSomething(tempL);
s.pop();
}
}
}
public void postorderStackSupport(TreeNode root,Stack s) {
while(root.left!=null)
{
s.push(root);
root = root.left;
}
s.push(root);
while (root.right!= null)
{
root = root.right;
while(root.left!=null)
{
s.push(root);
root = root.left;
}
s.push(root);
}
}
后序遍历的顺序是左儿子->右儿子->父结点,因此辅助函数需要作一些修改。对于前两种我们只需要以根节点为起点,沿父节点->左儿子的顺序知道某个节点没有左儿子即可。但现在我们不仅要求该节点没有左儿子,还要求该节点没有右儿子,否则该节点就不是第一个应该被处理的节点(因为这样的话就是父节点了),因此有了如图的实现。
借助辅助函数,我们成功按后序遍历的顺序将元素入栈,但为了防止我们无限对某一节点调用辅助函数,因此需要判断出栈的节点是否是当前栈顶的右儿子,如果是的话,说明已经对栈顶调用过辅助函数,我们再次出栈,然后处理。
Morris Traversal实现
前面两种遍历方式都使用了O(n)的空间,而这种遍历方式只需要O(1)的空间。(或许这就是大佬的思维方式吧)
算法的思路是:通过将结点的前驱结点指向自身来完成树的遍历。其实为什么要利用前驱结点呢?是因为树实际上是由许多单向链表组成,因此我们无法直接从儿子结点跳到父结点。而利用前驱结点正是一种解决这个问题的技巧。
//先序遍历
public void preorderMorris(TreeNode root){
TreeNode cur = root;
TreeNode pre = null;
while(cur!=null)
{
doSomething(cur);
if(cur.left==null)
cur = cur.right;
else
{
pre = cur.left;
while(pre.right!=null&&pre.right!=cur)
pre = pre.right;
if(pre.right==null)
{
pre.right = cur.right;
cur = cur.left;
}
else{
pre.right = null;
cur = cur.right;
}
}
}
}
在先序遍历中,前驱结点是它左儿子沿node->node.right路径上的最后一个结点。
1.先输出再说。然后判断左儿子是否为空,为空的话也就没有前驱节点了。
2.如果有左儿子,那么找到自己的前驱结点或找到当前节点(这说明前驱结点已经连接了当前结点)。
a.若找到前驱结点,则将前驱结点和当前节点连接,然后可以放心的将当前结点置为当前结点的左儿子。(后面可以通过连接回到这个结点)
b.若找到当前节点,则解除连接,然后将当前结点置为当前结点的右儿子(因为已经连接过了,说明当前结点的左子树已遍历完)
//中序遍历
public void inorderMorris(TreeNode root){
TreeNode cur = root;
TreeNode pre = null;
while(cur!=null)
{
if(cur.left==null)
{
doSomething(cur);
cur = cur.right;
}
else
{
pre = cur.left;
while(pre.right!=null&&pre.right!=cur)
pre = pre.right;
if(pre.right==null)
{
pre.right = cur;
cur = cur.left;
}
else{
pre.right = null;
doSomething(cur);
cur = cur.right;
}
}
}
}
中序遍历和先序遍历比较类似,就不赘述了。
//后序遍历
public void postorderMorris(TreeNode root){
TreeNode htemp = new TreeNode(0);
htemp.left = root;
TreeNode cur = htemp;
TreeNode pre = null;
while(cur!=null)
{
if(cur.left==null)
cur = cur.right;
else
{
pre = cur.left;
while(pre.right!=null&&pre.right!=cur)
pre = pre.right;
if(pre.right==null)
{
pre.right = cur;
cur = cur.left;
}
else{
printReverse(cur.left,pre);
pre.right = null;
cur = cur.right;
}
}
}
}
//输出翻转链表
public void printReverse(TreeNode from,TreeNode to) {
reverse(from,to);
TreeNode temp = to;
while(temp!=from)
{
doSomething(temp);
temp = temp.right;
}
doSomething(temp);
reverse(from,to);
}
//翻转链表
public void reverse(TreeNode from,TreeNode to){
TreeNode x = from;
TreeNode y = to;
TreeNode z;
while(x!=to)
{
z = y.right;
y.right = x;
x = y;
y = z;
}
}
后续遍历的实现同样比较复杂,需要借助一个额外结点htemp和辅助函数reversePrint(),reverse()来倒序输出链表。
后序遍历的前驱结点依旧和前两种遍历方式一样。
1.判断当前节点的左儿子是否为空,若为空说明没有前驱结点。
2.若左儿子不为空,那么找到自己的前驱结点或找到当前节点(这说明前驱结点已经连接了当前结点)。
a.若找到前驱结点,则将前驱结点和当前节点连接,将当前结点置为当前结点的左儿子。(保证了左儿子最优先处理的顺序)
b.若找到当前结点自己,则倒序输出当前结点的左儿子与前驱结点之间的路径上的所有结点。(即保证了右儿子->父结点的顺序)再将当前结点置为当前结点的右儿子。至此终于算是总结完了,总觉得总结的没到点上,可能还是因为理解的不够透彻吧。