二叉树的的定义:二叉树是一棵空树或者每个节点最多只能有两个子树的有序树,左边的称为左子树,右边的称为右子树。
二叉树遍历是二叉树的基础知识。
二叉树的递归遍历非常简单,主要在于理解和掌握二叉树的非递归遍历以及递归转非递归的方法。
前言
二叉树节点定义如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
文中的代码均在leetcode提交通过。
前序遍历
递归方式
先序递归遍历时,先访问根节点,再先序递归遍历左子树,最后先序递归遍历右子树。递归就是套娃,不用管套娃的细节,只要套住子树的根节点就OK了。
在写递归方法时,第一步雷打不动先写循环退出的条件。
下面的写法中,保存访问过程的list是通过方法参数传递,亦可以把list作为类的成员变量。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList();
preorderTraversal(root, result);
return result;
}
public void preorderTraversal(TreeNode root, List<Integer> result) {
if (root == null) {
return;
}
result.add(root.val);
preorderTraversal(root.left, result);
preorderTraversal(root.right, result);
}
}
非递归方式
先序是三种遍历方式的非递归写法中最简单的。
要注意的是:
- 根节点先入栈;
- 循环条件是栈空;
- 当前节点的右子节点先入栈,左子节点后入栈。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
stack.offerFirst(root);
while (!stack.isEmpty()) {
TreeNode t = stack.pollFirst();
result.add(t.val);
if (t.right != null) {
stack.offerFirst(t.right);
}
if (t.left != null) {
stack.offerFirst(t.left);
}
}
return result;
}
}
中序遍历
递归方式
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
if (Objects.isNull(root)) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>();
inorderTraversal(root, result);
return result;
}
public void inorderTraversal(TreeNode root, List<Integer> result) {
if (Objects.isNull(root)) {
return;
}
inorderTraversal(root.left, result); // (1)
result.add(root.val); // (2)
inorderTraversal(root.right, result); // (3)
}
}
非递归方式
观察上面中序遍历的递归方法。(1)中将当前节点的左节点递归地入栈,即先将t.left迭代入栈到t = null,(1)执行结束即表示栈中弹出一帧,然后(2)中访问当前节点。最后当前节点变为t.right,以同样的方式处理t.right。
写法一:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
if (Objects.isNull(root)) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode t = root;
while (!stack.isEmpty() || t != null) {
while (t != null) {
stack.offerFirst(t);
t = t.left;
}
t = stack.pollFirst();
result.add(t.val);
t = t.right;
}
return result;
}
}
写法二: 方法的一次递归调用对应下面的一次while循环。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
if (Objects.isNull(root)) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode t = root;
while (!stack.isEmpty() || t != null) {
if (t != null) {
stack.offerFirst(t);
t = t.left;
} else {
t = stack.pollFirst();
result.add(t.val);
t = t.right;
}
}
return result;
}
}
后序遍历
递归方式
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>();
postorderTraversal(root, result);
return result;
}
public void postorderTraversal(TreeNode root, List<Integer> result) {
if (root == null) {
return;
}
postorderTraversal(root.left, result);
postorderTraversal(root.right, result);
result.add(root.val);
}
}
非递归方式
后序非递归遍历在中序的基础上进行修改,与中序不同的是,后序是先访问左子树,左子树遍历完成之后peek一眼根节点,然后又去访问右子树,访问完了右子树,最后再输出根节点的值。此时访问根节点时,为了不会再次进入右子树,需要用一个pre变量来保存上次访问的节点。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) {
return Collections.emptyList();
}
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode t = root;
TreeNode pre = null;
while (!stack.isEmpty() || t != null) {
if (t != null) {
stack.offerFirst(t);
t = t.left;
} else {
t = stack.peekFirst();
t = t.right;
if (t != null && t != pre) {
stack.offerFirst(t);
t = t.left;
} else {
t = stack.pollFirst();
res.add(t.val);
pre = t;
t = null;
}
}
}
return res;
}
}
二叉树层序遍历
层序遍历即广度优先搜索,借助队列完成。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return Collections.emptyList();
}
List<List<Integer>> result = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
TreeNode last = root;
TreeNode nlast = null;
TreeNode t = null;
List<Integer> list = new ArrayList<>();
while (!queue.isEmpty()) {
t = queue.poll();
list.add(t.val);
if (t.left != null) {
queue.offer(t.left);
nlast = t.left;
}
if (t.right != null) {
queue.offer(t.right);
nlast = t.right;
}
if (t == last) {
result.add(list);
list = new ArrayList<>();
last = nlast;
}
}
return result;
}
}
运用递归解决问题
二叉树是递归定义的。递归是解决树相关问题最有效和最常用的方法之一。
通常,我们可以通过自顶向下(尾递归)或自底向上(头递归)的递归来解决问题。
自顶向下
自顶向下意味着在每个递归层级,我们首先访问节点来计算一些值,并在递归调用函数时将这些值传递到子节点上。所以自顶向下的解决方案可以被认为是一种先序遍历。
特点:
- 在递归调用前先计算,将计算结果传入下一层;
- 计算结果通过方法入参传递。
递归函数f(root, param)是这样的:
1. if (root == null) return;
2. if (root.left == null && root.right == null) update and return;
3. f(root.left, leftParam);
4. f(root.right, rightParam);
param用来记录每个分支的值,当遍历到叶子节点时,根据param来更新最终的结果。
例如:求二叉树的最大深度。
从根节点出发,将当前的高度param传递给两个子节点。当左右子节点都为null时说明到达了叶节点,若从根节点到此叶节点的这条路径的高度比当前高度值height大,则更新高度值。
class Solution {
private int height = 0;
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
upToDown(root, 1);
return height;
}
public void upToDown(TreeNode root, int param) {
if (root == null) {
return;
}
if (root.left == null && root.right == null) {
height = Math.max(param, height);
}
upToDown(root.left, param + 1);
upToDown(root.right, param + 1);
}
}
自底向上
自底向上意味着在每个递归层级上,我们首先对所有子节点递归地调用函数,然后根据返回值和根节点本身的值得到答案。这个过程可以看作是后序遍历的一种。
特点:
- 在调用下一层时没有计算,直到下一层返回时才进行计算;
- 计算结果通过函数返回值传递。
递归函数f(root)是这样的:
1. if (root == null) return;
2. leftResult = f(root.left);
3. rightResult = f(root.right);
4. return result;
class Solution {
public int maxDepth(TreeNode root) {
return downToUp(root);
}
public int downToUp(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(downToUp(root.left), downToUp(root.right)) + 1;
}
}