二叉树是面试中常见的题目类型,这里,针对 leetcode 前200道题中的二叉树相关问题进行总结与思考,力求烂熟于心,能根据题目名回忆起题目的具体描述,进而用简洁的语言描述出算法的核心。具体的题目答案可打开 leetcode 查看历史提交记录。
二叉树的遍历(binary-tree-traversal)
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
为什么研究二叉树的遍历?
因为计算机只会处理线性序列,而我们研究遍历,就是把树中的结点变成某种意义的线性序列,这给程序的实现带来了好处。
1 三种基本遍历方式
首先是二叉树的遍历方式,最简单基础的也就是前序(preorder)、中序(inorder)、后序(postorder)三种遍历方式,对应题目 144.二叉树的前序遍历 、94. 二叉树的中序遍历 、145. 二叉树的后序遍历。
前中后,指的是根节点相对于左右结点的访问顺序
先序:考察到一个节点后,即刻输出该节点的值,并继续遍历其左右子树。(根左右)
中序:考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树。(左根右)
后序:考察到一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值。(左右根)
这种类型的题一般都是有两种解题思路:递归和迭代。
1.1 递归法模板
我们并不需要太过于在意具体的递归过程,而是要想清楚让计算机干什么。计算机都可能溢出,用人脑去遍历就不现实了。
思路: 判断什么时候把结点的 val
加入 res
数组中,res.add(root.val)
放的位置决定了遍历顺序。
1 2 3 4 5 6 7 8 9
| 边界条件
定义 dfs 函数: 如果root为空:返回 递归左子树 递归右子树
执行递归函数,返回结果
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Solution { public List<Integer> preorderTraversal(TreeNode root) { List<Integer> res=new ArrayList<>(); helper(root,res); return res; } private void helper(TreeNode root,List<Integer> res) { if(root==null) return; helper(root.left,res); helper(root.right,res); } }
|
一样的代码,稍微调用一下位置就可以,如此固定的套路,使得只掌握递归解法并不足以令面试官信服=`=所以必须掌握迭代法加深对二叉树的理解!!
1.2 迭代法模板
本质上是在模拟递归,因为在递归的过程中使用了系统栈,所以在迭代的解法中常用Stack
来模拟系统栈。
思路:用while
循环先将根节点和所有左孩子加入栈和结果数组中,直至cur
为空。然后,每弹出一个栈顶元素 tmp
,就到达它的右孩子,再将这个节点当作 cur
重新按上面的步骤来一遍,直至栈为空。这里又需要一个 while
循环。
使用这个迭代模板处理前序和中序遍历,只要更改 res.add(cur.val)
的位置即可。
1 2 3 4 5 6 7 8 9 10 11 12
| 边界条件 初始化stack,res,cur while stack或cur 非空: while cur非空: cur的值入栈stack cur 向左下或右下遍历 弹出节点tmp cur回到tmp的左或右子树 返回结果
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public List<Integer> preorderTraversal(TreeNode root) { List<Integer> res=new ArrayList<>(); Deque<TreeNode> stack = new LinkedList<>(); TreeNode cur=root; while(cur!=null||!stack.isEmpty()){ while(cur!=null){ stack.push(cur); cur=cur.left; } tmp=stack.pop(); cur=tmp.right; } return res; } }
|
但是后序并不能使用,取巧的方法是使用模板(根右左),对其取反Collections.reverse(res)
就是后序遍历的结果(左右根)
利用了结果的特点反向输出,不免显得技术含量不足,因此掌握后序遍历标准的栈操作解法是必要的!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public List<Integer> postorderTraversal(TreeNode root) { List<Integer> res=new ArrayList<>(); Deque<TreeNode> stack=new LinkedList<>(); TreeNode cur=root; TreeNode p=null; while(cur!=null || !stack.isEmpty()){ while(cur!=null){ stack.push(cur); cur=cur.left; } cur=stack.peek(); if(cur.right==null||cur.right==p){ res.add(cur.val); stack.pop(); p=cur; cur=null; }else{ cur=cur.right; } } return res; } }
|
2 层序遍历及其变题
除了基本的三种遍历使用DFS,使用BFS广度优先搜索的层序遍历也很重要102. 二叉树的层序遍历
下面是两种解决此类问题的方法,一般使用左边的BFS
2.1 BFS层序遍历
BFS遍历的标准写法如下,使用的是队列数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13
| void bfs(TreeNode root) { Queue<TreeNode> queue = new ArrayDeque<>(); queue.add(root); while (!queue.isEmpty()) { TreeNode node = queue.poll(); if (node.left != null) { queue.add(node.left); } if (node.right != null) { queue.add(node.right); } } }
|
我们可以直接用 BFS 得出层序遍历结果。然而,层序遍历要求的输入结果和 BFS 是不同的。层序遍历要求我们区分每一层,也就是返回一个二维数组。而 BFS 的遍历结果是一个一维数组,无法区分每一层,见下图的对比。
因此,我们需要在每一层前微修改一下代码,在每一层遍历开始前,先记录队列中的结点数量 n(也就是这一层的节点数量),然后一口气处理完这一层的 n个结点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>(); if (root != null) { queue.add(root); } while (!queue.isEmpty()) { int n = queue.size(); List<Integer> level = new ArrayList<>(); for (int i = 0; i < n; i++) { TreeNode node = queue.poll(); level.add(node.val); if (node.left != null) { queue.add(node.left); } if (node.right != null) { queue.add(node.right); } } res.add(level); }
return res; }
|
这样,我们就可以用BFS来解决层序遍历问题了,重点是记录了每一层节点的数量,动图如下
2.2 (选看)DFS层序遍历
DFS 遍历标准写法如下,使用的是递归
1 2 3 4 5 6 7
| void dfs(TreeNode root) { if (root == null) { return; } dfs(root.left); dfs(root.right); }
|
看起来比BFS的代码简洁很多,但观看下图就可以发现两者的遍历顺序是不同的,BFS其实更贴近我们想要的层序遍历逻辑。
那DFS究竟可不可以解决层序遍历问题呢?当然是可以的!
把这个二叉树的样子调整一下,摆成一个田字形的样子。
田字形的每一层就对应一个 list,每次递归的时候都需要带一个 index(表示当前的层数),也就对应那个田字格子中的第几行,如果当前行对应的 list 不存在,就加入一个空 list 进去。代码实现如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import java.util.*; class Solution { public List<List<Integer>> levelOrder(TreeNode root) { if(root==null) { return new ArrayList<List<Integer>>(); } List<List<Integer>> res = new ArrayList<List<Integer>>(); dfs(1,root,res); return res; } void dfs(int index,TreeNode root, List<List<Integer>> res) { if(res.size()<index) { res.add(new ArrayList<Integer>()); } res.get(index-1).add(root.val); if(root.left!=null) { dfs(index+1, root.left, res); } if(root.right!=null) { dfs(index+1, root.right, res); } } }
|
感觉有点像二叉树的前序遍历,根左右
该题是leetcode103题,medium难度,一句话题干描述:层序遍历,不过是之字形的。
总体思路:用个变量flag来判断是从左往右还是从右往左打印
解决方案1(容易理解): 在层序遍历代码的基础上,更改节点数值进入list的顺序
1 2 3 4 5
| if (leftToRight) { level.add(node.val); } else { level.add(0, node.val); }
|
解决方案2(画图理解): 使用deuqe双端队列:可以在两边同时添加和删除的队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| # Deque<TreeNode> deque=new LinkedList<>(); if (leftToRight) { cur = deque.removeFirst(); if (cur.right != null) deque.addLast(cur.right); if (cur.left != null) deque.addLast(cur.left); } else { cur = deque.removeLast(); if (cur.left != null) deque.addFirst(cur.left); if (cur.right != null) deque.addFirst(cur.right); } level.add(cur.val);
|
deque中把每一行的节点值反序列表示,然后每次打印判断从左往右读deque还是从右往左。读的时候相应的也要改变左右孩子入队列的顺序来保持 deque中每一行的节点值反序表示。
该题是leetcode199题,medium难度,一句话题干描述:从上到下返回每一层最后一个元素
总体思路:利用 BFS 进行层次遍历,记录下每层的最后一个元素。
解决方案1(BFS): 在层序遍历代码基础上,判断当前节点是不是这层的最后一个节点,是的话才添加进结果数组。
1
| if(i==n-1) res.add(cur.val);
|
解决方案2(DFS):我们按照 「根结点 -> 右子树 -> 左子树」 的顺序访问,就可以保证每层都是最先访问最右边的节点的。(与先序遍历 「根结点 -> 左子树 -> 右子树」 正好相反,先序遍历每层最先访问的是最左边的节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Solution { List<Integer> res= new ArrayList<>(); public List<Integer> rightSideView(TreeNode root) { dfs(root,0); return res; } private void dfs(TreeNode root,int depth) { if(root==null) return ; if(depth==res.size()){ res.add(root.val); } depth++; dfs(root.right,depth); dfs(root.left,depth); } }
|
该题是leetcode116题,medium难度,一句话题干描述:使得每个节点的next指针指向它右侧的节点(完全二叉树)
总体思路:利用 BFS 进行层次遍历队列,队列中保存了第 i 层节点的信息,我们利用这个特点,将队列中的元素都串联一遍就可以了。
解决方案1(基础版):使用队列,直接把层序遍历的代码改一改, 把每一层的节点连接起来,这种方法同样适用于不是完全二叉树的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| while(!queue.isEmpty()){ int size=queue.size(); Node pre = null; for (int i = 0; i < size; i++) { Node node = queue.poll(); if (pre != null) { pre.next = node; } pre = node; if (node.left != null) queue.add(node.left); if (node.right != null) queue.add(node.right); } }
|
上面运行效率并不是很高,这是因为我们把节点不停的入队然后再不停的出队,其实可以不需要队列,每一行都可以看成一个链表比如第一行就是只有一个节点的链表,第二行是只有两个节点的链表
解决方案2(优化版):观察这样的串联树可以发现,有两种了连接方式
- 两个串联的节点都有一个共同的父节点,通过父节点就可以将这两个子节点串联起来
- 这两个串联的节点的父节点不同,对于这种情况,如果我们能将这一层的上一层串联好。那么可以通过父节点的
next
找到邻居,完成串联。即 root.right.next => root.next.left
,这里我们需要保证 root.next 不为空就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Node pre=root;
while(pre.left!=null){ Node tmp=pre; while(tmp!=null){ tmp.left.next = tmp.right; if(tmp.next!=null) { tmp.right.next = tmp.next.left; } tmp = tmp.next; } pre = pre.left; }
|
解决方案3(递归骚操作):递归从上往下,先处理当前节点,再处理它的左、右子树中的节点——前序遍历。
递归终止条件:当遍历到叶子节点,没有孩子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public Node connect(Node root) { dfs(root); return root; } void dfs(Node root){ if(root==null) return ; if(root.left!=null){ root.left.next = root.right; if (root.next!=null){ root.right.next = root.next.left; } } connect(root.left); connect(root.right); }
private void dfs(Node node, Node next) { if(node != null) { node.next = next; dfs(node.left, node.right); dfs(node.right, node.next != null ? node.next.left : null); } }
|
总结:**每个 node 左子树的 next , 就是 node 的右子树、每个 node 右子树的 next, 就是 node next 的 左子树**
该题是leetcode117题,medium难度,一句话题干描述:使得每个节点的next指针指向它右侧的节点(非完全二叉树)
总体思路:和上一题一样的思路
解决方案1(基础版):使用队列,直接把层序遍历的代码改一改, 把每一层的节点连接起来,同上一题代码,略。
解决方案2(优化版):更改上一题的相关代码,增加哑节点(找到下一层的第一个节点),此代码也可用于上题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Node cur=root;
while(cur!=null){ Node dummy = new Node(0); Node pre = dummy; while(cur!=null){ if(cur.left!=null){ pre.next=cur.left; pre=pre.next; } if(cur.right!=null){ pre.next=cur.right; pre=pre.next; } cur = cur.next; } cur = dummy.next; }
|
此外,除了这些题外,还有以下题
637.二叉树的层平均值
429.N叉树的前序遍历
515.在每个树行中找最大值
参考资料:leetcode题解,感谢这些大佬的配图以及易懂的语言!!