二叉树的三种常用遍历方式
学习过数据结构的同学都清楚,除了层序遍历外,二叉树主要有三种遍历方式:
1. 先序遍历
2. 中序遍历
3. 后序遍历
关于这三种遍历方式,我不做赘述,** 网上或者书本上,大家可以去了解一下。 ** 这次我们主要总结一下二叉树遍历的算法的递归实现和非递归实现。而重点又落在非递归实现上,因为,递归实现,其实也是书本上的基础知识,只要去查阅机会有所理解。
递归实现
先序遍历的递归实现
我们先定义一个二叉树的节点结构作为本博文中的通用节点。
public class Node{
public int value;
public Node left;
public Node right;
public Node(int data){
this.value = data;
}
}
我们知道,二叉树的递归遍历无非是对一颗非空的二叉树的每一个节点,做相同的操作(先序、中序、后序)。二叉树的先序遍历就是对每一个节点来先序遍历的动作。代码如下:
public void preOrder(Node head){
if(head == null)return;
System.out.print(head.value + " ");
preOrder(head.left);
preOrder(head.right);
}
中序遍历的递归实现
同样的,二叉树中序遍历的递归实现,也是建立在同样的思想上。
public void midOrder(Node head){
if(head == null)return;
midOrder(head.left);
System.out.print(head.value);
midOrder(head.right);
}
后序遍历的递归实现
** The same idea , not the same recipe. **
public void posOrder(Node head){
if(head == null)return;
posOrder(head.left);
posOrder(head.right);
System.out.print(head.value);
}
关于递归实现的简单总结
从上面的代码,我们发现,在递归实现上,三种遍历方式的主要区别就是输出当前头结点的值的时机,从而让遍历的顺序产生区别。
非递归实现
用递归方法解决的问题都可以用非递归来实现,因为递归方法无非是实用函数栈来保存信息,如果用自己定义的栈来代替函数栈,就可以实现相同的功能。
先序遍历的非递归实现
先来大致分析一下非递归方式实现先序遍历的具体过程:
1. 申请一个新的stack,然后把头结点打入stack中
2. 从stack中弹出栈顶节点,记为 head ,然后打印 head 的值,然后 ** 把 head 的右孩子压入栈中,再把 head 的左孩子压入栈中(注意先后顺序)。 ** 当然在做这些操作之前,都需要一个 ** 非空判断. ** 。
3. 重复步骤2,直到 stack 为空。
到这里,我们贴上一段代码:
public void preOrderNoRecur(Node head){
if(head != null){
//新建一个栈空间,存储节点,对应过程中的第一步
Stack<Node> stack = new Stack<Node>();
//把头结点压入栈中
stack.add(head);
while(! stack.isEmpty()){
head = stack.pop();
System.out.print(head.value + " ");
//分别对弹出节点的右孩子、左孩子判空,按照顺序压入栈中
if(head.right != null)stack.add(head.right);
if(head.left != null)stack.add(head.left)
}
}
}
** 需要重点理解的就是弹出节点的左右孩子被压入栈的时机。 **建议大家自行画一个图,模拟一下程序运行的过程。
中序遍历的非递归实现
同样的,我们先分析一下中序遍历非递归实现的具体过程:
1. 申请一个新的 stack 。** 跟先序遍历不同的是 ** ,我们需要两个值,一个cur,记录当前遍历到的节点的值,一个head,记录当前二叉树的根节点。** 初始时,cur = head ** 。
2. 把cur代表的节点压入栈中,让cur = cur.left
3. 重复步骤2,直到 cur 为空时,从 stack 中弹出一个节点,记为 outNode ,打印 outNode 的值。然后让 cur = outNode.right,继续重复步骤2。
4. 当 stack 为空且 cur 为空时,整个遍历完成。闲话不多说,上代码。
public void midOrderNoRecur(Node head){
if(head != null){
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while(!stack.isEmpty() || cur != null){
if(cur != null){
//步骤二,不停的把以 cur 的左孩子压入栈中
stack.push(head);
cur = cur.left;
}else{
//当 cur 为空时,我们弹出栈顶元素,然后对其右孩子做相同的事
Node outNode = stack.pop();
System.out.print(outNode.value + " ");
cur = outNode.right;
}
}
}
}
** 实际上, ** 我们可以把这段程序中的 cur , outNode都替换成head,这样做可以节约空间,但是在代码清晰度上个人觉得会有所降低。因为是用来总结,所以觉得还是把各个东西写清晰的好。但是也附上都替换成 head 后的代码。
public void midOrderNoRecur(Node head){
if(head != null){
Stack<Node> stack = new Stack<Node>();
while(!stack.isEmpty() || head != null){
if(head != null){
//步骤二,不停的把以 head 的左孩子压入栈中
stack.push(head);
head = head.left;
}else{
//当 head 为空时,我们弹出栈顶元素,然后对其右孩子做相同的事
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
}
此处预留一个位置,今天实在太晚了,困的不行,接下面的明天继续更。Good Night.
更新......
后序遍历的非递归实现
关于二叉树后序遍历的非递归实现,我学习了两种方式。一种是利用两个栈,另一种是利用一个栈。
两个栈实现后序遍历
依然从具体步骤开始:
1. 申请两个 stack,分别记为 s1,s2 ,然后将头结点压入 s1 中
2. 从 s1 中弹出一个节点,记为 cur ,然后 cur 的左孩子和右孩子按顺序压入栈中
3. 把 cur 压入 s2 中
4. 不断重复第二步、第三步、一直到 s1 为空时停止
5. 从 s2 中依次弹出节点并打印值
public void posOrderNoRecur(Node head){
if(head != null){
//申请两个栈空间
Stack<Node> s1 = new Stack<Node>();
Stack<Node> s2 = new Stack<Node>();
//把头结点压入栈s1中
s1.push(head);
while(!s1.isEmpty()){
//从s1中弹出一个节点,并且分别对其左右孩子判空,压入s1
Node cur = head.pop();
//把弹出的节点压入s2
s2.push(cur);
if(cur.left != null) s1.push(cur.left);
if(cur.right != null) s2.push(cur.right);
}
while(!s2.isEmpty())
System.out.print(s2.pop().value + " ");
}
}
** 以上,我们便利用两个栈完成了二叉树后序遍历的非递归实现,思想上很简单,就是对每个节点做一次先序遍历,然后再按照右孩子,左孩子的顺序压入 s2 中,因为我们知道栈的特性之一就是先进后出,按照前面所说的顺序把节点压入栈中,再遍历时就会得到一个正确的后序遍历。同样建议大家自己在稿纸上画图,模拟过程,帮助理解。 **
一个栈实现后序遍历
** 明天继续... **
用一个栈来实现二叉树的后序遍历,多少会有一些麻烦,我决定采用先贴出代码的方式。
public void posOrderNoRecur(Node h){
if(h != null){
Stack<Node> stack = new Stack<Node>();
stack.push(h);
Node c = null;
while(!stack.isEmpty()){
c = stack.peek();
if(c.left != null && h != c.left && h != c.right){
stack.push(c.left);
}else if(c.right != null && h != c.right){
stack.push(c.right);
}else{
System.out.print(stack.pop().value + " ");
h = c;
}
}
}
}
然后再来说代码的实现思路,主要是遵从这样的步骤:
1. 首先,依然是申请一个栈,记为 stack , 并且设置两个变量 h 和 c。在整个过程中,h 代表最近一次弹出并打印的节点,c 代表栈顶节点,初始情况: h 为 head , c 为 null。
2. 这个时候(h 为最新弹出节点,c 为栈顶节点< ** 并没有弹出 ** >)有三种情况:
* 如果 c 的左孩子不为空,且 h 不是 c 的左孩子也不是 c 的右孩子,那么就把 c 的左孩子压入栈中。
* 如果 c 的又孩子不为空,且 h 不是 c 的右孩子,那我们就把 c 的右孩子压入栈中。
* 如果上述两种情况都不满足,说明 c 的左子树和右子树都已经打印完毕,然后让 h = c.
* 结束条件: 栈 stack 为空
接下来我们解释上述步骤的逻辑,首先,我们刚开始的时候把 h 代表的头结点压入 stack 中,此时,栈顶节点即为树的根,我们另 c = stack.peek(),这个时候,按照我们对后序遍历的理解,我们应该先遍历头结点的左子树,如果 c 的 ** 左孩子不为空 ** 的话,这个时候我们的 h 肯定不等于 c 的左孩子/右孩子,因为我们并没有把左孩子/右孩子压入栈中,所以我们把 c 的左孩子压入栈中,而之所以判断一下 h 所代表的刚弹出的节点是否为 c 的左孩子/右孩子,是因为我们要判断 c 的左右孩子是否有遍历。如果有遍历,那我们自然可以判断, c 的左右孩子已经遍历过,弹出 c 节点打印值,然后进入下一次循环即可。同理的,当 c 的左孩子为空,右孩子不为空时,我们自然要判断刚弹出的是否是 c 的右孩子,然后判断把 c 的右孩子压入栈中。
我们讲了很多关于压入栈的操作,但是我们什么时候输出呢,也就是什么时候得到 h 的值呢。最后一个 else 包含的情况有哪些呢。很明显,就是遍历到叶子节点的时候。当我们碰到栈顶节点是叶子节点的时候,就会产生弹出。
总结
本文主要讲了二叉树非常基础的三种遍历方式: 先序遍历、中序遍历、后序遍历。在实现方式上,我们主要区分递归的实现方式和非递归的实现方式。递归的方式是书本上的内容,非递归方式的话,其实都很好理解,主要应该注意的就是压入栈的时机,左右孩子压入栈的顺序。这些顺序的判断和分析,都是建立在先/中/后序遍历的特点上的。只要清楚所应该遵循的规则,相信就可以写出正确的程序。
Ps: 花了四天才写完这篇博客。第一天太晚,第二天感冒,第三天出去浪了。是时候提高一波执行力了。
** 博客地址 :** 博客