二叉树遍历
如果你之前学过二叉树,你一定清楚二叉树的遍历方式有四种,分别为前序遍历、中序遍历、后序遍历、层序遍历。而二叉树遍历除层序遍历位通常使用递归的方式来实现。
递归代码如下:
//前序遍历
public void preorderTraversal(TreeNode head) {
if(head == null)return;
System.out.println(head.val);//前序遍历放在前面
preorderTraversal(head.left);
preorderTraversal(head.right);
}
//中序遍历
public void preorderTraversal(TreeNode head) {
if(head == null)return;
preorderTraversal(head.left);
System.out.println(head.val);//中序遍历放在中间
preorderTraversal(head.right);
}
//后序遍历
public void preorderTraversal(TreeNode head) {
if(head == null)return;
preorderTraversal(head.left);
preorderTraversal(head.right);
System.out.println(head.val);//后序遍历放在后面
}
使用递归代码遍历二叉树非常简单,根据前序、中序、后序遍历分别将输出放在前面、中间、后面就行了。
如果面试要让你实现二叉树的遍历,一定会问你迭代的方式如何实现。就算不是为了面试,如果二叉树的深度非常深,就有可能造成栈溢出异常,这个时候就需要考虑迭代的方式。那么迭代的方式如何实现呢,这里以前序遍历为例,如图求它的前序遍历结果:
使用递归法很简单,但是迭代应该怎么做呢?我们知道方法的调用本质是使用了栈,那我们能不能直接使用栈来模拟遍历二叉树的过程呢。代码如下:
public List<Integer> preorderTraversal(TreeNode head) {
//保存二叉树遍历结果
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(head);
while (!stack.empty()){
TreeNode next = stack.pop();
result.add(next.val);
if (next.right != null)stack.push(next.right);
if (next.left != null)stack.push(next.left);
}
return result;
}
前序遍历的顺序是: 根节点 ---> 左子树 ---> 右子树 ,在上述代码中我们用栈结构来模拟遍历过程:
- 判断栈中是否还有节点,如果没有节点则直接返回,否则进入第二步
- 将栈首的节点出栈
- 判断其右子树、左子树是否为null,不为null则加入栈中,由于栈是先进后出的数据结构,所以先判断右子树,再判断左子树。之后继续第一步。
同理使用栈实现二叉树的中序遍历的代码如下:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null)return result;
Stack<TreeNode> stack = new Stack<>();
do{
while(root != null){
stack.push(root);
root = root.left;
}
if(!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
root = node.right;
}
}while(!stack.isEmpty() || root != null);
return result;
}
中序遍历的顺序是: 左子树 ---> 根节点 ---> 右子树,算法流程如下:
- 先遍历到root结点的最左侧结点,也就是当root为空时,栈顶的元素为其子树的根结点,这里是确保栈顶元素为最左侧的节点
- 弹出栈顶元素,并获取当前节点的右子树。当前节点的右子树可能为 null ,所以在 while 循环,有两个判断条件。继续第一步。
由于后序遍历的结果是前序遍历的逆序,所以后序遍历的迭代的代码有严格的和不严格的,代码如下:
- 不严格的逆序遍历
//不严格的后序遍历,只是遍历结果和后序遍历结果相同
//但是如果需要按照后序遍历的顺序对树节点进行访问(或操作),这种方式就不行了
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
if(root == null)return result;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(0,node.val);
if(node.left != null)stack.push(node.left);
if(node.right != null)stack.push(node.right);
}
return result;
}
- 严格的后序遍历
代码来源leetcode的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) return Collections.emptyList();
List<Integer> res = new ArrayList<>(); //保存结果
Stack<TreeNode> call = new Stack<>(); //调用栈
call.push(root); //先将根结点入栈
while (!call.isEmpty()) {
TreeNode t = call.pop();
if (t != null) {
call.push(t);
call.push(null);//这里用null作为标志来判断是否遍历过
if (t.right != null) call.push(t.right);
if (t.left != null) call.push(t.left);
} else {
res.add(call.pop().val);
}
}
return res;
}
}
我们可以看出二叉树两种遍历的方式的特点:
- 递归法:实现简单,但是耗时一般比迭代法高,如果二叉树深度深容易发生栈溢出异常
- 迭代法:耗时一般比递归法短,不会出现栈溢出异常,但是实现复杂,尤其是中序遍历、后序遍历
那有没有一种更好的遍历的方法,不会出现栈溢出异常,而且实现起来比较简单,最好是像递归那样,实现方法有规律,容易记忆。
二叉树的前序遍历非常简单,代码非常像递归实现。那为什么中序遍历和后序遍历和后序遍历就这么复杂呢。二叉树的前序遍历顺序是根节点 ---> 左子树 ---> 右子树,它遍历根节点只需要保存根节点的数据即可,而中序遍历的顺序是左子树 ---> 根节点 ---> 右子树,由于先根节点--->左子树,然后回退到根节点,会重复遍历到根节点,它需要确保根节点不会被重复使用,不能就会造成死循环了。那我们能不能使用一个集合来保存节点的已遍历的节点,每次遍历时判断一下该节点是否遍历过。代码如下:
//中序遍历代码
public List<Integer> inorderTraversal(TreeNode root) {
ArrayList<Integer> res = new ArrayList<>();
if (root == null) {
return outputs;
}
Stack<TreeNode> stack = new Stack<>();
HashSet<TreeNode> visited = new HashSet<>();//记录该节点是否被访问过
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (visited.contains(node)) {
res.add(node.val);
} else {
if (node.right != null)stack.push(node.right);
stack.push(node);
if (node.left != null)stack.push(node.left);
visited.add(node);
}
}
return res;
}
怎么样代码是不是好理解多了,这种方式最厉害的是将前序、中序、后序遍历的代码实现进行了统一。代码如下:
//前序遍历的迭代方式
public List<Integer> preorderTraversal(TreeNode root) {
...//上面的代码和中序遍历相同,这里就不展示了
} else {
if (node.right != null)stack.push(node.right);
if (node.left != null)stack.push(node.left);
stack.push(node);//栈保存节点的位置放在后面,与递归的方式相反
visited.add(node);
}
}
return res;
}
//后序遍历的迭代方式,这里是严格的方式
public List<Integer> postorderTraversal(TreeNode root) {
...//上面的代码和中序遍历相同,这里就不展示了
} else {
stack.push(node); //栈保存节点的位置放在前面,与递归的方式相反
if (node.right != null)stack.push(node.right);
if (node.left != null)stack.push(node.left);
visited.add(node);
}
}
return res;
}
如果你不想用HashMap来保存状态的更厉害的方法,可以看看完全模仿递归,不变一行.
二叉树的遍历总算结束了
我们发现二叉树的遍历无论是递归法还是迭代法,其空间复杂度都为O(n).那能不能将空间复杂度降到O(1)呢?这就需要讲到线索二叉树和莫里斯(Morris)遍历了
如图(图片来源深入学习二叉树(二) 线索二叉树):
线索二叉树:若结点的左子树为空,则该结点的左孩子指针指向其前驱结点;若结点的右子树为空,则该结点的右孩子指针指向其后继结点。
莫里斯(Morris)遍历可以看二叉树的莫里斯(Morris)遍历(写的时候才发现有这个东西😓,之后熟悉了再补上)
二叉树的遍历基础
前序遍历
前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。
如图所示:
遍历结果为: FBADCEGIH
中序遍历
中序遍历是先遍历左子树,然后访问根节点,然后遍历右子树。
如图所示:
遍历结果为:ABCDEFGHI
后序遍历
后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根节点。
如图所示:
后序遍历的结果为: ACEDBHIGF
层序遍历
层序遍历就是逐层遍历树结构。
如图所示:
层序遍历结果为:FBGADICEH
参考
- leetcode的前序遍历、中序遍历、后序遍历、层序遍历的题解
- 深入学习二叉树(二) 线索二叉树
- 二叉树的莫里斯(Morris)遍历