Java实现二叉树的先序、中序、后序遍历算法
一、构建二叉树的存储结构
一棵二叉树的节点结构主要包括三部分:节点的值域,指向左孩子的指针,指向右孩子的指针。由于Java语言没有指针的定义,这里采用属性的方式实现(重写equals方法是为了后续遍历非递归算法的需要,后面会说明)。代码如下:
import java.util.Objects;
/**
* @author:Chris
* @description 二叉树结点结构
*/
public class BiTNode {
char val; //节点的值域
BiTNode lChild; //指向左孩子
BiTNode rChild; //指向右孩子
public BiTNode(char val) {
this.val = val;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BiTNode biTNode = (BiTNode) o;
return val == biTNode.val &&
Objects.equals(lChild, biTNode.lChild) &&
Objects.equals(rChild, biTNode.rChild);
}
}
二、先序、中序与后续遍历的递归算法
2.1 先序遍历
简单来说,就是从一棵树的根节点开始,先访问根节点,然后依次访问左孩子和右孩子节点。也就是“根左右”的次序。
举个例子,如图2.1所表示的二叉树。从根节点出发依次遍历,A节点为根节点,访问A节点;然后遍历左孩子B节点,由于B节点也是所在子树的根节点,所以访问B节点;再继续遍历B节点的左孩子,由于为空,不做处理;继续遍历B节点的右孩子,访问D节点;D节点的左右孩子均为空,不做处理。这样,以A节点为根的左子树就遍历结束了。再依次遍历A节点的右子树,最后得到的先序遍历序列为:A - B - D - C - E
代码如下:
/**
* 先序遍历(递归)
* @param bt
*/
public void PreOrder(BiTNode bt) {
if (bt != null) {
System.out.print(bt.val + " ");
PreOrder(bt.lChild);
PreOrder(bt.rChild);
}
}
图2.1 二叉树示例
2.2 中序遍历
中序遍历的访问次序为:左、根、右
如图2.1所示,从根节点出发,节点A左孩子非空,则应向左遍历;遇到节点B,遍历节点B的左孩子,发现左孩子为空;此时再次回到节点B,访问节点B;由于节点B的右孩子非空,继续向右遍历;节点D的左右孩子均为空,访问节点D;此时以节点A为根的左子树均被访问过了,然后访问节点A,然后遍历A的右子树。最终的访问次序为:B - D - A - C - E。需要注意的是,这里“遍历”这个词,更准确的描述应该是“经过”或“路过”。与先序遍历相比,先序遍历第一次路过根节点,根节点即被访问;而中序遍历时,根节点被第二次路过时,才会被访问。
代码如下:
/**
* 中序遍历(递归)
* @param bt
*/
public void InOrder(BiTNode bt) {
if (bt != null) {
InOrder(bt.lChild);
System.out.print(bt.val + " ");
InOrder(bt.rChild);
}
}
2.3 后续遍历
后续遍历的访问次序为:左、右、根
还是以图2.1为例,从根节点出发,优先向左遍历。首次经过节点B,继续向左 (注意:虽然节点B的左子树为空,但是也需要进行一次判断,即经过一次)。节点B左子树为空,继续遍历节点B的右子树 (此时节点B被第二次经过)。对于节点D,左右子树均为空,访问节点D。再次回到节点B,访问节点B (此时节点B被第三次经过时,才被访问)。此时以节点A为根的左子树就均被访问过了。继续遍历节点A的右子树,最后访问节点A,遍历结束。最终访问的次序为:D - B - E - C - A
代码如下:
/**
* 后序遍历(递归)
*
* @param bt
*/
public void PostOrder(BiTNode bt) {
if (bt != null) {
PostOrder(bt.lChild);
PostOrder(bt.rChild);
System.out.print(bt.val + " ");
}
}
三、初始化一棵二叉树
这里将采用先序遍历的方式构造一棵二叉树,如图2.1所示。初始化输入各节点时,将空节点记为:’#’。
/**
* 先序遍历构造二叉树
* @param bt
* @return BiTNode
*/
public BiTNode Create(BiTNode bt) {
Scanner scanner = new Scanner(System.in);
String str = scanner.next();
char s = str.charAt(0);
if (s == '#') {
bt = null;
} else {
bt = new BiTNode(s);
bt.lChild = Create(bt.lChild);
bt.rChild = Create(bt.rChild);
}
return bt;
}
四、测试
新建一个测试类名为 TreeTest
,将上述遍历操作放在名为 TreeExer
的类下面。在 TreeTest
的main方法中进行测试。代码如下:
public class TreeTest {
public static void main(String[] args) {
TreeExer treeExer = new TreeExer();
BiTNode root = new BiTNode();
root = treeExer.Create(root);
//先序遍历递归算法
treeExer.PreOrder(root);
//中序遍历递归算法
// treeExer.InOrder(root);
//后续遍历递归算法
// treeExer.PostOrder(root);
}
}
以图2.1为例,数据录入,输出先序遍历递归算法的结果如图4.1。(其他算法的调用读者可以自行尝试)
图4.1 先序遍历递归算法测试
五、先序、中序、后序遍历的非递归算法
将递归算法改写为非递归算法,往往需要栈这种数据结构。其实递归算法底层的实现也是通过栈来记录每次调用函数的位置。
idea中可以在System.out.print(bt.val + " ");
处打断点进行调试,可以清楚的看到递归工作栈中递归调用的信息,如图5.1。
5.1 先序遍历(非递归)
先序遍历的非递归算法也是借助栈来实现。由于是先访问根节点,所以可以选择在入栈之前,对根节点进行访问。
代码如下:
/**
* 先序遍历的非递归算法
*
* @param bt
*/
public static void PreOrder(BiTNode bt) {
Stack<BiTNode> st = new Stack<>(); //用于保存节点
BiTNode p = bt; //工作指针
while (p != null || !st.isEmpty()) {
if (p != null) {
visit(p);
st.push(p); //这里也可以使用add(Object obj)方法,线程安全的。因为Stack类继承自Vector类
p = p.lChild;
} else {
p = st.pop(); //如果节点为空,栈顶元素出栈
p = p.rChild; //指向右子树
}
}
}
图5.1 断点调试
5.2 中序遍历(非递归)
与先序遍历的非递归算法唯一不同的是,visit
函数的位置不同。这里是出栈后,对目标节点进行访问。也对应了前面提到的"第二次经过"才对其访问。
代码如下:
/**
* 中序遍历的非递归算法
*
* @param bt
*/
public static void InOrder(BiTNode bt) {
Stack<BiTNode> st = new Stack<>(); //用于保存节点
BiTNode p = bt; //工作指针
while (p != null || !st.isEmpty()) {
if (p != null) {
st.push(p); //这里也可以使用add(Object obj)方法,线程安全的。因为Stack类继承自Vector类
p = p.lChild;
} else {
p = st.pop(); //如果节点为空,栈顶元素出栈
visit(p);
p = p.rChild; //指向右子树
}
}
}
/**
* 访问该节点
* @param bt
*/
private static void visit(BiTNode bt) {
System.out.print(bt.val + " ");
}
5.3 后续遍历(非递归)
在后续遍历中,要保证左孩子和右孩子都已经被访问,并且左孩子要在右孩子前访问,最后再访问根节点,这就为流程控制带来了困难。
问题的关键在于,当经过根节点时如何判断是从左子树返回的还是从右子树返回的。因此,声明一个指针r,它用于记录最近访问过的节点。也可以在类BiTNode
中增加一个标志属性,记录是否已经被访问过。这里采用第一种方式实现。
代码如下:
/**
* 后续遍历的非递归算法
*
* @param bt
*/
public static void PostOrder(BiTNode bt) {
BiTNode p = bt; //工作节点,用于访问每个节点
BiTNode r = null; //记录前一个被访问的节点
Stack<BiTNode> st = new Stack<>();
while (p != null || !st.isEmpty()) {
if (p != null) {
st.push(p); //节点非空,则入栈
p = p.lChild; //指向左子树
} else {
p = st.peek(); //读栈顶元素
if (p.rChild != null && p.rChild.equals(r) == false) { //右孩子节点非空,且未被访问
p = p.rChild; //转向右子树
st.push(p);
p = p.lChild; //再次指向左孩子
} else { //右子树为空,或已经访问过
p = st.pop(); //栈顶元素出栈
visit(p);
r = p; //记录最近访问过的节点
p = null; //注意:p需要置空
}
}
}
}
六、总结
先序遍历、中序遍历和后续遍历是二叉树相关问题的核心操作,也是很多题目的核心思想。其中后序遍历非递归算法应用场景有很多,例如:求根节点到某节点的路径、求两个节点的最近公共祖先等等。