二叉树的非递归遍历
2016-11-28 22:27
183 查看
学过数据结构的同学都知道二叉树有三种常见的遍历方式,分别为前序遍历、中序遍历、后序遍历,而且这三种遍历的方式的递归编程都比较好写,然而他们的非递归遍历则不是那么的容易。
下面就介绍一下树的三种遍历的非递归写法。
前序遍历
遍历顺序是:根节点->左儿子->右儿子
思路很简单:建立一个栈,先把非null的根节点放进去,进入while循环,
只要栈不为空,就pop一个对象出来,然后输出该节点的元素,并且判断它的右儿子不为空则入栈,左儿子不为空则入栈。(注意这里入栈的顺序,因为输出的顺序是根->左->右,所以入栈的顺序是根->右->左)
第二种先序遍历的方法的思路是:
建立一个栈,然后直接进入循环体判断:如果当前节点不为空或者是栈不为空则循环继续,先判断当前节点是否为空,若不为空,则输出节点元素,把当前节点入栈,并把左儿子设为当前节点,进行下一次的循环判断;如果当前节点为空,即意味着栈不为空,从栈中pop一个节点,并把改节点的右儿子作为当前节点,进行下一次的循环判断。
逻辑上很好理解,就是当某个节点有左儿子的时候一直输出左儿子,直到为空则取上一个父节点的右儿子,重复这样的操作。
中序遍历
中序遍历的顺序:左儿子->根节点->右儿子
从上面的先序遍历中可以借鉴得到中序遍历的非递归写法。思路如下:
建立栈,然后直接进入循环体判断:如果当前节点不为空或者是栈不为空,则循环体继续。进入循环体后,进入第二个while循环体,只要当前节点不为空,则把当前节点入栈,然后设置该节点的左儿子作为当前节点。当前节点为空,则退出内部的循环体,然后从栈中pop一个节点,并输出,设置该节点的右儿子作为当前节点,紧接着进行外循环判断。
这个程序很巧妙地运用了输出节点的条件:
当某个节点没有左儿子的时候(宏观上可以想象成左子树遍历完了)就可以输出了,然后遍历他的右儿子。
当遍历到某个节点的右儿子为空时(宏观上可以想象成右子树遍历完了),则可以输出该节点的父亲节点了
后序遍历
后序遍历的顺序:左儿子->右儿子->根节点
可以看到这个方法中用到了一个栈来记录访问节点的顺序,还有一个map,它的作用是:试想访问父节点的时候,需要判断左右儿子是否都已经输出了,才能将父亲节点输出。
这个方法很巧妙,因为对于一个节点有四种情况:
有左儿子,有右儿子
有左儿子,无右儿子
无左儿子,有右儿子
无左儿子,无右儿子
那么对于任意的一个节点,他的输出条件就是:
有左儿子但已经遍历,有右儿子但已经遍历
有左儿子但已经遍历,无右儿子
无左儿子,有右儿子但已经遍历
无左儿子,无右儿子
好了,明白了这样的事实之后,那么我们再来看,如果一个节点有左儿子,并且没有遍历,那么肯定要把它的左儿子入栈,并且是一直访问到他的左子树最深的一个左儿子处才停止。
对一个节点,如果判断了他的左子树为null或者已经遍历完了,那么紧接着就要判断他的右子树了。如果一个节点有右儿子,并且没有遍历,那么只要把他的右儿子入栈,并且直接进入下一次的循环体判断就好。
后序遍历的一个投机的做法是将前序遍历中code one的代码稍作修改:把根节点的左右儿子的压栈顺序改变一下,然后跑一遍非典型的“先序遍历”代码,最后将整个输出倒一下就是后序遍历的结果。
注意:如果你是仅仅为了实现后序遍历这个功能,那么这样的做法很巧妙,但是如果你把这个做为面试官要你回答后序遍历的非递归程序的答案的话,有待考虑,因为这样的做法对后序遍历的逻辑思路似乎暴露不多。
总结
可以注意到二叉树的非递归遍历都应用到了栈这个数据结构,其实很自然,因为递归遍历其实底层用的不就是栈么。那么区别在哪里?
个人觉得,用递归的话代码比较清晰,但是代价是牺牲了宝贵的内存资源,搞不好会内存崩溃(递归程度比较深的尾递归,程序中有时会遇到内存崩溃有可能就是这个问题),而且像斐波那契数列递归做法那样有可能会重复计算。用循环的话,内存资源比较省,一个局部变量占的内存资源是O(1),代价是需要自己维护一个栈。
下面就介绍一下树的三种遍历的非递归写法。
前序遍历
遍历顺序是:根节点->左儿子->右儿子
// code one private void nonRecursivePreOrder(BinaryNode<Integer> t) { Stack<BinaryNode<Integer>> treeStack = new Stack<>(); if (t!=null) { treeStack.push(t); while(!treeStack.isEmpty()){ BinaryNode<Integer> tmp = treeStack.pop(); System.out.println(tmp.element+" "); if (tmp.righttree!=null) { treeStack.push(tmp.righttree); } if (tmp.lefttree!=null) { treeStack.push(tmp.lefttree); } } } }
思路很简单:建立一个栈,先把非null的根节点放进去,进入while循环,
只要栈不为空,就pop一个对象出来,然后输出该节点的元素,并且判断它的右儿子不为空则入栈,左儿子不为空则入栈。(注意这里入栈的顺序,因为输出的顺序是根->左->右,所以入栈的顺序是根->右->左)
//code two private void nonRecursivePreOrder(BinaryNode<Integer> t) { Stack<BinaryNode<Integer>> treeStack = new Stack<>(); BinaryNode<Integer> current = t; while(current!=null || !treeStack.isEmpty()) { if(current!=null) { System.out.println(current.element); treeStack.push(current); current = current.lefttree; }else { current = treeStack.pop(); current = current.righttree; } } }
第二种先序遍历的方法的思路是:
建立一个栈,然后直接进入循环体判断:如果当前节点不为空或者是栈不为空则循环继续,先判断当前节点是否为空,若不为空,则输出节点元素,把当前节点入栈,并把左儿子设为当前节点,进行下一次的循环判断;如果当前节点为空,即意味着栈不为空,从栈中pop一个节点,并把改节点的右儿子作为当前节点,进行下一次的循环判断。
逻辑上很好理解,就是当某个节点有左儿子的时候一直输出左儿子,直到为空则取上一个父节点的右儿子,重复这样的操作。
中序遍历
中序遍历的顺序:左儿子->根节点->右儿子
private void nonRecursiveInOrder(BinaryNode<Integer> t) { Stack<BinaryNode<Integer>> treeStack = new Stack<>(); BinaryNode<Integer> current = t; while (current!=null || !treeStack.isEmpty()) { while (current!=null) { treeStack.push(current); current = current.lefttree; } current = treeStack.pop(); System.out.println(current.element); current = current.righttree; } }
从上面的先序遍历中可以借鉴得到中序遍历的非递归写法。思路如下:
建立栈,然后直接进入循环体判断:如果当前节点不为空或者是栈不为空,则循环体继续。进入循环体后,进入第二个while循环体,只要当前节点不为空,则把当前节点入栈,然后设置该节点的左儿子作为当前节点。当前节点为空,则退出内部的循环体,然后从栈中pop一个节点,并输出,设置该节点的右儿子作为当前节点,紧接着进行外循环判断。
这个程序很巧妙地运用了输出节点的条件:
当某个节点没有左儿子的时候(宏观上可以想象成左子树遍历完了)就可以输出了,然后遍历他的右儿子。
当遍历到某个节点的右儿子为空时(宏观上可以想象成右子树遍历完了),则可以输出该节点的父亲节点了
后序遍历
后序遍历的顺序:左儿子->右儿子->根节点
//code one private void notRecursivePostOrder(BinaryNode<Integer> t) { Stack<BinaryNode<Integer>> treeStack = new Stack<>(); Map<BinaryNode<Integer>, Boolean> map = new HashMap<>(); treeStack.push(t); while(!treeStack.isEmpty()) { BinaryNode<Integer> current = treeStack.peek(); if (current.lefttree!=null && !map.containsKey(current.lefttree)) { current = current.lefttree; while (current!=null) { treeStack.push(current); current = current.lefttree; } continue; } if (current.righttree!=null && !map.containsKey(current.righttree)) { treeStack.push(current.righttree); continue; } current = treeStack.pop(); System.out.println(current.element); map.put(current, true); } }
可以看到这个方法中用到了一个栈来记录访问节点的顺序,还有一个map,它的作用是:试想访问父节点的时候,需要判断左右儿子是否都已经输出了,才能将父亲节点输出。
这个方法很巧妙,因为对于一个节点有四种情况:
有左儿子,有右儿子
有左儿子,无右儿子
无左儿子,有右儿子
无左儿子,无右儿子
那么对于任意的一个节点,他的输出条件就是:
有左儿子但已经遍历,有右儿子但已经遍历
有左儿子但已经遍历,无右儿子
无左儿子,有右儿子但已经遍历
无左儿子,无右儿子
好了,明白了这样的事实之后,那么我们再来看,如果一个节点有左儿子,并且没有遍历,那么肯定要把它的左儿子入栈,并且是一直访问到他的左子树最深的一个左儿子处才停止。
对一个节点,如果判断了他的左子树为null或者已经遍历完了,那么紧接着就要判断他的右子树了。如果一个节点有右儿子,并且没有遍历,那么只要把他的右儿子入栈,并且直接进入下一次的循环体判断就好。
//code two private void notRecursivePostOrder(BinaryNode<Integer> t) { Stack<BinaryNode<Integer>> treeStack = new Stack<>(); List<Integer> result = new ArrayList<>(); treeStack.push(t); while (!treeStack.isEmpty()) { BinaryNode<Integer> current = treeStack.pop(); if (current!=null) { result.add(current.element); treeStack.push(current.lefttree); treeStack.push(current.righttree); } } for (int i=result.size()-1; i>=0; i--) { System.out.println(result.get(i)); } }
后序遍历的一个投机的做法是将前序遍历中code one的代码稍作修改:把根节点的左右儿子的压栈顺序改变一下,然后跑一遍非典型的“先序遍历”代码,最后将整个输出倒一下就是后序遍历的结果。
注意:如果你是仅仅为了实现后序遍历这个功能,那么这样的做法很巧妙,但是如果你把这个做为面试官要你回答后序遍历的非递归程序的答案的话,有待考虑,因为这样的做法对后序遍历的逻辑思路似乎暴露不多。
总结
可以注意到二叉树的非递归遍历都应用到了栈这个数据结构,其实很自然,因为递归遍历其实底层用的不就是栈么。那么区别在哪里?
个人觉得,用递归的话代码比较清晰,但是代价是牺牲了宝贵的内存资源,搞不好会内存崩溃(递归程度比较深的尾递归,程序中有时会遇到内存崩溃有可能就是这个问题),而且像斐波那契数列递归做法那样有可能会重复计算。用循环的话,内存资源比较省,一个局部变量占的内存资源是O(1),代价是需要自己维护一个栈。
相关文章推荐
- 二叉树的非递归遍历
- 二叉树递归与非递归遍历
- 二叉树的非递归遍历 C语言版
- 二叉树的深度优先遍历、广度优先遍历和非递归遍历
- 二叉树的先序、中序、后序、层序递归及非递归遍历
- 二叉树的非递归遍历
- 二叉树的非递归遍历(不用栈、O(1)空间)
- 遍历二叉树的各种操作(非递归遍历)
- 二叉树的创建|非递归遍历
- 二叉树的非递归遍历
- 二叉树的非递归遍历
- 二叉树的非递归遍历
- 二叉树的非递归遍历
- 二叉树的递归与非递归遍历
- 二叉树的非递归遍历
- 二叉树的非递归遍历 C语言版
- Java实现二叉树的创建、递归/非递归遍历
- 二叉树前序、中序和后序的非递归遍历
- 二叉树的实现(递归遍历和非递归遍历)C++
- 二叉树的递归遍历与非递归遍历