本文主要从递归的遍历原理分析遍历的流程以及三种遍历方式的相同不同之处,并依此写出非递归的共同流程,在此基础上实现非递归的三种遍历方式。
首先,给出树类的定义:
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
递归的先、中、后序遍历代码如下:
先序遍历:
public static void preOrder(TreeNode root){
if(root==null)
return ;
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
中序遍历:
public static void inOrder(TreeNode root){
if(root==null)
return;
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
后序遍历:
public static void postOrder(TreeNode root){
if(root==null)
return;
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val + " ");
}
可以看出,若是去掉打印语句System.out.print(root.val + " ");
,上述三段代码没有任何不同,而仅仅通过打印语句的位置不同,就分别实现了递归的三种遍历方式。
我们先去掉打印语句单独看以下这段代码:
public static void Order(TreeNode root){
if(root==null)
return;
Order(root.left);
Order(root.right);
}
- 它的作用是:
- 如果当前结点为空,则直接返回,如果不为空,执行2;
- 递归访问它的左孩子;
- 递归访问它得右孩子;
而这里可以发现,对于每个结点,这个方法会遍历它三次,
1. 刚进入这个方法
2. 递归访问完左孩子后返回
3. 递归访问完右孩子后返回
实现不同的三种遍历方式的方式的本质就在于,当第几次访问该结点时我们对其进行记录,而根据上述三次进入则分别成了先序、中序、后序遍历。因此,我们根据这个特性来归纳非递归的三种遍历方式代码段。
非递归写法肯定是用到栈,这点先不做详细解释。
对于栈中每个元素,如果仅算它入栈和出栈操作,则每个元素访问次数应该为2次,那我们先得到先序和中序遍历的共同代码段(即他们共同的访问路径)如下:
public static void Order(TreeNode root){//非递归
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode p = root;
while(p!=null||!stack.isEmpty()){
while(p!=null){//如果结点不为空,插入栈
stack.push(p);
p = p.left;//往左孩子走(类似递归中访问左孩子)
}
p = stack.pop();//当左孩子为空,则弹出当前结点(类似递归中递归完左孩子后返回该方法)
p = p.right;//往右孩子走(类似递归中访问右孩子)
}
}
而根据递归方式中的打印语句的位置,插入到非递归方式中则成为:
先序:
while(p!=null||!stack.isEmpty()){
while(p!=null){//如果结点不为空,插入栈
System.out.print(p.val + " ");//打印(记录遍历)
stack.push(p);
p = p.left;//往左孩子走(类似递归中访问左孩子)
}
p = stack.pop();//当左孩子为空,则弹出当前结点(类似递归中递归完左孩子后返回该方法)
p = p.right;//往右孩子走(类似递归中访问右孩子)
}
中序:
while(p!=null||!stack.isEmpty()){
while(p!=null){//如果结点不为空,插入栈
stack.push(p);
p = p.left;//往左孩子走(类似递归中访问左孩子)
}
p = stack.pop();//当左孩子为空,则弹出当前结点(类似递归中递归完左孩子后返回该方法)
System.out.print(p.val + " ");//打印(记录遍历)
p = p.right;//往右孩子走(类似递归中访问右孩子)
}
这样,我们就得到了先序和中序的非递归遍历方式。而我们为什么使用栈仅对每个结点访问两次就可以实现递归方式中每个结点访问三次的功能呢?
因为先序和中序遍历是在前两次访问就记录了遍历这一操作,所以第三次访问该结点时实际上无任何操作方法就直接结束了,所以我们可以略去第三次访问得到该代码。
而后序遍历的访问路径和上述方法相同,但后序是在第三次访问该结点时记录,而上述的方法只会访问每个结点两次,我们该如何解决这个问题?
我们可以利用栈的另一基本操作——返回栈的栈顶元素而不移除来增加一次访问,当第二次访问到该结点时我们不让它出栈,而是只通过它来访问右孩子,直到右孩子访问结束再出栈并打印。实现这一点,我们首先要考虑,如何分辨我们是第二次还是第三次访问该结点。
因此,我们需要一个flag数组标记来记录我们是第几次访问该结点,以此来确定我们对其操作是pop()
还是peek()
。
public static void postOrder(TreeNode root){
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode p = root;
int flag[] = new int[20];//标记数组,记录我们是第几次访问该结点(数组长度大于结点个数)
while(p!=null||!stack.isEmpty()){
while(p!=null){//第一次访问该结点,执行与上述相同
stack.push(p);
p = p.left;
}
p = stack.peek();
if(p.right!=null&&flag[stack.size()]==0){//访问该结点并判断是第几次访问
flag[stack.size()] = 1;//若是第二次(入栈访问过一次),则不移除该结点并将访问次数更新
p = p.right;
}
else{
flag[stack.size()] = 0;//若是第三次访问,则先更新该结点对应的记录访问值,并移除出栈
p = stack.pop();
System.out.print(p.val + " ");//每次出栈打印并将p置为null,防止下一轮while的错误判断
p = null;
}
}
}
由此可见,后序遍历的非递归方式与先、中相似,只是在左孩子访问完后进行了多一轮判断确认该不该直接弹出。
总结:
- 明确递归方式的原理,从而清楚递归的真正流程;
- 根据递归的流程确定每个结点实际是三次访问,而在不同的访问时刻记录则得到了不同的遍历方式;
- 从该原则出发,得到非递归的基本流程,在这基本流程上在分别实现三种遍历的记录时刻。