0 前言
1 准备工作
public class TreeUtil {
public static Node makeTree() { //生成二叉树
ArrayList<Node> nodes = new ArrayList<Node>();
for (int i = 0; i < 5; i++) {
nodes.add(new Node(i));
}
Node root = nodes.get(0);
root.left = nodes.get(1);
root.right = nodes.get(2);
nodes.get(1).left = nodes.get(3);
nodes.get(1).right = nodes.get(4);
return root;
}
}
public class Node { //结点
Node left;
Node right;
int value;
public Node(int value) {
this.value = value;
}
}
2 前序遍历
前序遍历应该是前中后非递归遍历中,最容易实现的遍历方式。主要原因是,在访问某个结点时,可以立即打印当前结点,从而不需要在遍历栈中保存这个结点的状态。
图1 前序遍历
图2表示了这棵树的前序遍历结点访问顺序。初始化遍历的引用指向0号结点。第1个访问的结点是0号结点,这时可以直接打印出0号结点,然后开始对0号结点的左子树进行遍历。0号结点的左子树的根节点为1号结点,根据前序遍历的规则,那么下一个访问的结点应该是1号结点。对于0号结点而言,由于它的右子树还没有进行遍历,之后的回溯过程应该要回到2号结点继续进行遍历的。所以,当引用指向0号结点的同时,应该同时将2号结点进行进栈保存。同理,当下一个访问1号结点时,应该将4号结点进栈保存,而遍历的引用指向3号结点。当引用指向3号结点时,栈的状态如图3所示:
图2 前序遍历访问3号结点时的栈状态
由于3号结点已经是叶子结点,此时需要进行回溯。回溯是通过对遍历栈进行出栈操作完成。所以,通过一步出栈操作,从栈中弹出4号结点。于是,将当前的引用指向4号结点。打印4号结点后,发现4号结点又是叶子结点,那么再进行出栈操作,从栈中弹出2号结点。同理,引用指向2号结点,并再次发现当前处于叶子结点,于是再次进行出栈操作。但是,此时的遍历栈已经为空,所以无法进行出栈操作,那么退出循环,结束遍历。此时刚好遍历完图2整棵树,打印顺序为0,1,3,4,2 。
现在,对上述过程进行一下总结:
1.对于前序遍历,每当访问一个结点时,先打印结点。
2.如果存在右子树,那么将右子树的根节点进行进栈保存,否则忽略。
3.如果存在左子树,将遍历引用指向左子树根节点,否则出栈回溯。
4.循环的退出条件是需要出栈操作时,栈为空,无法进行该操作。
public class PreOrderTraversal {
private static Stack<Node> stack = new Stack<Node>();
private static ArrayList<Integer> result = new ArrayList<Integer>();
public static void main(String[] args) {
Node root = TreeUtil.makeTree();
preOrderTraversal(root);
System.out.println(result);
}
private static void preOrderTraversal(Node root) {
Node currentNode = root;
while (true) {
saveResultNode(currentNode);
if (currentNode.right != null) {
stack.push(currentNode.right);
}
if (currentNode.left != null) {
currentNode = currentNode.left;
} else {
try {
currentNode = stack.pop();
} catch (EmptyStackException e) {
currentNode = null;
}
}
if (currentNode == null) {
break;
}
}
}
private static void saveResultNode(Node node) { //保存需要打印的结点
result.add(node.value);
}
}
3 中序遍历
但是中序遍历的遍历顺序为 左子树->根结点->右子树 ,也就是说第一次访问到某个结点时,不会立刻打印当前结点,需要左子树完成后再打印根节点,然后访问右子树。这样的话,在整个中序遍历的过程中,对于非叶子结点,每个结点需要被访问两次。比如,对于图3中1号结点,第一次被访问是在从0号结点访问左子树时,此时1号结点的左子树(即3号结点)还没有被遍历;第二被访问是1号左子树遍历并打印后,回溯时被访问到的,此时1号结点的左子树已经遍历完成。显然,在第一次访问1号结点时,应该保存1号结点的状态,等到第二次被访问时打印1号结点。这里和前序遍历不同点在于,前序遍历在第一次访问某个非叶子结点时,保存的是这个结点的右子树状态,而此时的中序遍历需要保存这个结点的状态。原因前面已经讲过, 前序遍历时对树的访问和遍历有一定的一致性,即第一次访问结点时就可以打印出当前结点状态,从而不需要保存当前结点状态,直接保存右子树的根节点即可。而中序遍历是当第二次访问这个结点时才会被打印出来,因此需要将结点保存到第二次被访问的时候。
这里可能会有一点疑问:如果中序遍历访问某个结点时,保存的是当前结点而不是右孩子结点,那么当第二次访问并打印这个结点后,怎么进行右子树的遍历?其实,由于在前序遍历时,第一次访问非叶子结点就可以打印,因此规避掉了这个结点的左右子树是否遍历完成的问题。而中序遍历时,想要判断某个非叶子结点是被第一次访问还是第二次访问,就必须有一种办法可以判断当前结点左子树是否被遍历完成(后面的后序遍历不仅仅要判断左子树是否遍历完成,还需要判断右子树也同样遍历完成)。这里想到的方法是,当每次打印结点时,同时将这个结点保存在HashSet中(代码中使用的是HashSet,实际上用数组也可以,只是这里觉得java的HashSet已经有了现成的接口,更方便使用)。每一次访问结点的同时,也同时查看一下这个HashSet中是否包含了这个结点的左孩子结点。如果没有包含,则将访问引用指向左孩子,同时将当前结点进栈保存;否则打印当前结点,将访问引用指向右孩子(如果存在的话,否则继续出栈回溯)。
public class InOrderTraversal {
private static Stack<Node> stack = new Stack<Node>();
private static HashSet<Node> resultsMap = new HashSet<Node>();
private static ArrayList<Integer> result = new ArrayList<Integer>();
public static void main(String[] args) {
Node root = TreeUtil.makeTree();
inOrderTraversal(root);
System.out.println(result);
}
private static void inOrderTraversal(Node root) {
Node currentNode = root;
while (true) {
// 判断左子树是否被遍历。
// 如果左子树为空,则认为左子树被遍历
if(isLeftSubtreeTraversaled(currentNode)){
saveResultNode(currentNode);
if(currentNode.right!=null){
currentNode=currentNode.right;
}else{
try {
currentNode = stack.pop();
} catch (EmptyStackException e) {
currentNode = null;
}
}
}else{
stack.push(currentNode);
currentNode=currentNode.left;
}
if (currentNode == null) {
break;
}
}
}
/**
* 将需要打印的结点按顺序保存在result中,同时也记录在resultsMap中。resultsMap可以快速查询出某个结点是否已经被打印。
* 如果考虑存储空间,可以只用result这个Arraylist来保存结果,代价是查询结点是否被打印时必须要逐个比较整个Arraylist,速度会慢一些。
*/
private static void saveResultNode(Node node) {
result.add(node.value);
resultsMap.add(node);
}
private static boolean isLeftSubtreeTraversaled(Node node) {
return node.left == null || resultsMap.contains(node.left);
}
}
1.对于中序遍历,第一次访问非叶子结点时,需要将当前结点进栈保存。
2.第二次访问时非叶子结点时,打印结点。继续出栈回溯还是将访问引用指向右孩子取决于右孩子是否存在。
3.每次打印结点,需要同时将打印结点保存,用于记录当前结点是否被遍历。
4.循环的退出条件是需要出栈操作时,栈为空,无法进行该操作。
4 后序遍历
public class PostOrderTraversal {
private static Stack<Node> stack = new Stack<Node>();
private static HashSet<Node> resultsMap = new HashSet<Node>();
private static ArrayList<Integer> result = new ArrayList<Integer>();
public static void main(String[] args) {
Node root = TreeUtil.makeTree();
postOrderTraversal(root);
System.out.println(result);
}
private static void postOrderTraversal(Node root) {
if (root == null) {
throw new IllegalArgumentException("root should not be null");
}
Node currentNode = root;
while (true) {
// 判断左子树是否被遍历。
// 如果左子树为空,则认为左子树被遍历
if (isLeftSubtreeTraversaled(currentNode)) {
// 同理于左子树
if (isRightSubtreeTraversaled(currentNode)) {
saveResultNode(currentNode);
try {
currentNode = stack.pop();
} catch (EmptyStackException e) {
currentNode = null;
}
} else {
stack.push(currentNode);
currentNode = currentNode.right;
}
} else {
stack.push(currentNode);
currentNode = currentNode.left;
}
if (currentNode == null) {
break;
}
}
}
private static void saveResultNode(Node node) {
result.add(node.value);
resultsMap.add(node);
}
private static boolean isLeftSubtreeTraversaled(Node node) {
return node.left == null || resultsMap.contains(node.left);
}
private static boolean isRightSubtreeTraversaled(Node node) {
return node.right == null || resultsMap.contains(node.right);
}
}
1.对于非叶子结点,只有当左右子树的根节点均打印完成后,才可以打印当前结点。
2.每次打印结点,需要同时将打印结点保存,用于记录当前结点是否被遍历。
3.循环的退出条件是需要出栈操作时,栈为空,无法进行该操作。
5 总结