本文旨在描述如何用迭代的方式一步一步实现二叉树后序遍历的过程!
我们先定义一个方法,参数为二叉树的根节点。方法内包含一个while循环。
private static void postOrderTraversal(TreeNode root) {
while(condition) {
}
}
还需要一个变量记录当前访问的节点,第一个访问的节点是根节点
private static void postOrderTraversal(TreeNode root) {
TreeNode cur = root;
while(condition) {
}
}
while循环的条件是什么呢?答案是还有需要访问的节点,也就是说当前访问的节点不为空。先不考虑,我们如何得到每次循环需要访问的节点。
private static void postOrderTraversal(TreeNode root) {
TreeNode cur = root;
while(cur != null) {
}
}
后序遍历是先后序遍历左子树,所以我们可以在每次主循环的开始,循环判断当前节点的左节点是否为空,如果不为空,则访问当前节点的左节点。
private static void postOrderTraversal(TreeNode root) {
TreeNode cur = root;
while(cur != null) {
while (cur.left != null) {
cur = cur.left;
}
}
}
通过当前代码,我们最终会找到二叉树的最左节点。我们还需要判断当前节点的右节点是否为空,如果不为空,则访问当前节点的右节点。此时,当前节点的右节点变成当前节点。
private static void postOrderTraversal(TreeNode root) {
TreeNode cur = root;
while(cur != null) {
while (cur.left != null) {
cur = cur.left;
}
if (cur.right != null) {
cur = cur.right;
}
}
}
通过当前代码,我们最终会得到一个没有左节点,也没有右节点的节点。也就是我们中序遍历需要访问的第一个节点。我们直接打印节点的值,表示我们访问过该节点。但是,我们无法通过左右节点得到其他可访问的节点。那是不是说循环就要结束了呢?当然不是,因为我们还没有遍历所有的节点。通过观察之前的循环,我们不难发现有部分节点我们只访问了它们的左节点,还有部分节点我们虽然访问了它的左右节点,但是自身却没有访问。现在我们需要先访问这些节点。但是由于二叉树没有指向父节点的引用,我们没有办法得到这些节点,所以我们需要将之前只访问过其左节点的那些节点保存起来。又由于我们访问这些节点的顺序跟之前循环的顺序正好相反,这符合栈先进后出的特性,所以我们使用栈来保存这些节点。
private static void postOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null) {
while (cur.left != null) {
stack.push(cur);
cur = cur.left;
}
if (cur.right != null) {
cur = cur.right;
} else {
System.out.print(cur.val + " ");
}
}
}
与前序和中序遍历不同,我们不仅要将只访问了其左节点的节点保存到栈中,那些访问了其右节点的节点也需要保存到栈中,因为节点本身还需要被访问。
private static void postOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null) {
while (cur.left != null) {
stack.push(cur);
cur = cur.left;
}
if (cur.right != null) {
stack.push(cur);
cur = cur.right;
} else {
System.out.print(cur.val + " ");
}
}
}
如果栈不为空,我们从栈顶弹出节点。如果该节点的右节点不为空,就访问该节点的右节点,并break当前循环。但是,因为此时我们还未访问过该节点,需要将该节点重新保存到栈中。此时,该节点的右节点变成当前节点。如果该节点的右节点为空,我们直接打印该节点的值,表示访问过该节点。继续从栈顶弹出节点。
private static void postOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null) {
while (cur.left != null) {
stack.push(cur);
cur = cur.left;
}
if (cur.right != null) {
cur = cur.right;
} else {
System.out.print(cur.val + " ");
while (!stack.isEmpty()) {
TreeNode tmp = stack.pop();
if (tmp.right != null) {
stack.push(cur);
cur = tmp.right;
break;
} else {
System.out.print(cur.val + " ");
}
}
}
}
}
}
因为我们之前的步骤中,将访问过其右节点的节点也保存到了栈中,当我们在进行栈的弹出操作时,如果只判断右节点不为空,会导致死循环。我们可以保存在栈弹出操作时上一次访问过的节点,来确保不会重复访问。
private static void postOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev;
while(cur != null) {
while (cur.left != null) {
stack.push(cur);
cur = cur.left;
}
if (cur.right != null) {
cur = cur.right;
} else {
System.out.print(cur.val + " ");
while (!stack.isEmpty()) {
TreeNode tmp = stack.pop();
if (tmp.right != null && tmp.right.equals(prev)) {
System.out.print(cur.val + " ");
prev = tmp;
}
if (tmp.right != null) {
stack.push(cur);
cur = tmp.right;
break;
} else {
System.out.print(cur.val + " ");
prev = tmp;
}
}
}
}
}
}
当然,如果栈是空的,那就表示没有需要访问的节点了。我们可以设置当前节点为空,或直接break主循环。还有一种情况,就是栈不为空,但是栈中所有节点的右节点都为空,此时也表示没有需要访问的节点了。我们也需要设置当前节点为空,或直接break主循环。这两种情况可以用一个逻辑来覆盖。
private static void postOrderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev;
while(cur != null) {
while (cur.left != null) {
stack.push(cur);
cur = cur.left;
}
if (cur.right != null) {
cur = cur.right;
} else {
System.out.print(cur.val + " ");
cur = null;
while (!stack.isEmpty()) {
TreeNode tmp = stack.pop();
if (tmp.right != null && tmp.right.equals(prev)) {
System.out.print(cur.val + " ");
prev = tmp;
}
if (tmp.right != null) {
stack.push(cur);
cur = tmp.right;
break;
} else {
System.out.print(cur.val + " ");
prev = tmp;
}
}
}
}
}
}
后序遍历
(1)后序遍历左子树。
(2)后序遍历右子树。
(3)访问根结点。