本文针对笔试/面试过程中常考察的一些 二叉树相关的操作进行整理,建议每一个都要掌握到 一看到题就想到思路,默写出来的程度。
本文参考了【代码随想录】的部分内容,苦于没有java版本的,因此整理。
二叉树的数据结构
数据结构的定义以及简单逻辑的代码一定要能手撕出来!
public class TreeNode{
int val;//节点值
TreeNode left;//左节点
TreeNode right;//右节点
TreeNode(){}//无参构造方法
TreeNode(int val){this.val = val;}//带参构造方法
TreeNode(int val, TreeNode left, TreeNode right){
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树遍历
遍历主要分为深度优先遍历:先往深⾛,遇到叶⼦节点再往回⾛;⼴度优先遍历:⼀层⼀层的去遍历。其中DFS又包括先中后三种方向,每个方向分别有递归(较简单)和迭代两中写法,面试问的话一般要求两个都会,下面依次介绍。
先序遍历-递归、迭代
首先是递归的写法。注意从递归的三要素来写,才能应对复杂问题。
- 确定递归函数的参数和返回值;
- 确定终⽌条件
- 确定单层递归的逻辑
遵循根节点->左子树->右子树的顺序遍历。
class Solution {//递归
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
preOrder(root, ans);
return ans;
}
public void preOrder(TreeNode root, List<Integer> ans){
if(root == null){
return;
}
ans.add(root.val);
preOrder(root.left, ans);
preOrder(root.right, ans);
}
}
这里我们思考一下,递归是怎么实现的?实际上,每⼀次递归调⽤都会把函数的局部变量、参数值和返回地址等压⼊调⽤栈中,然后递归返回的时候,从栈顶弹出上⼀次递归的各项参数。
这样,我们直接用栈这个结构,来做遍历,得到的就是迭代法。
针对先序遍历,其迭代法思想就是:对每个节点,先把右节点入栈,再把左节点入栈(这样出栈就是先左后右)。只要栈不为空,就一直循环下去,每次栈顶节点操作。
class Solution {//迭代
public List<Integer> preorderTraversal(TreeNode root) {
LinkedList<TreeNode> stack = new LinkedList<>();//定义栈
List<Integer> ans = new ArrayList<>();
if(root == null) return ans;
stack.addLast(root);
while(!stack.isEmpty()){
TreeNode node = stack.removeLast();//每次操作栈顶节点
ans.add(node.val);//中
if(node.right != null) stack.addLast(node.right);//右节点先入栈
if(node.left != null) stack.addLast(node.left);//左后
}
return ans;
}
}
先序遍历的迭代写法其实做了两件事:1.处理:将元素放入数组
,2.访问:遍历树
。本质上是DFS的思想:DFS一定要想到用栈来辅助操作,虽然可能没必要用!!!
中序遍历-递归、迭代
遵循左子树->根节点->右子树的顺序遍历。
差别主要就在递归函数的最后三行顺序。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
inOrder(root, ans);
return ans;
}
public void inOrder(TreeNode root, List<Integer> ans){
if(root == null){
return;
}
inOrder(root.left, ans);
ans.add(root.val);
inOrder(root.right, ans);
}
}
奇怪的是,对中序遍历的迭代写法,而不能随便在先序的基础上改改,还是有点思考度的。为什么先序遍历的迭代实现,和中序遍历不一致呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码, 因为要访问的元素和要处理的元素顺序是⼀致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是⼆叉树顶部的节点,然后⼀层⼀层向下访问,直到到达树左⾯的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不⼀致。解决方法:用一个临时节点来访问。
class Solution {//迭代
public List<Integer> inorderTraversal(TreeNode root) {
LinkedList<TreeNode> stack = new LinkedList<>();//定义栈
List<Integer> ans = new ArrayList<>();
if(root == null) return ans;
TreeNode cur = root;
while(cur != null || !stack.isEmpty()){//两种可能导致继续循环
if(cur != null){//1.还有节点没遍历到
stack.addLast(cur);
cur = cur.left;//实际上这里在处理左
}else{//2.栈里还有没处理
cur = stack.removeLast();
ans.add(cur.val);//中
cur = cur.right;//右
}
}
return ans;
}
}
后序遍历-递归、迭代
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
postOrder(root, ans);
return ans;
}
public void postOrder(TreeNode root, List<Integer> ans){
if(root == null){
return;
}
postOrder(root.left, ans);
postOrder(root.right, ans);
ans.add(root.val);
}
}
先序遍历是中左右,后续遍历是左右中,那么我们只需要调整⼀下先序遍历的代码顺序,就变成中右左的遍历顺序,然后反转结果数组,输出的结果顺序就是左右中了。妙不妙?
class Solution {//迭代
public List<Integer> postorderTraversal(TreeNode root) {
LinkedList<TreeNode> stack = new LinkedList<>();//定义栈
List<Integer> ans = new ArrayList<>();
if(root == null) return ans;
stack.addLast(root);
while(!stack.isEmpty()){
TreeNode node = stack.removeLast();//每次操作栈顶节点
ans.add(node.val);//中
if(node.left != null) stack.addLast(node.left);//左节点先
if(node.right != null) stack.addLast(node.right);//右后
}
Collections.reverse(ans);//反转集合
return ans;
}
}
层序遍历
我一直觉得层序遍历二叉树是特别简答好记的做法,一看到层序遍历,就要想到用队列来做。队列存什么?每层的节点啊。很妙:每次循环中,遍历某一行的节点操作,一个个出队,先把节点值存下来,然看看看有没有左子节点,有就入队,再看右子节点,有就入队。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
if(root == null) return ans;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.addLast(root);//第一行肯定只有根节点
while(!queue.isEmpty()){//循环结束条件:队列空了
int n = queue.size();//一定要搞个变量存下当前队列(层)节点数量
List<Integer> list = new ArrayList<>();
for(int i = 0; i < n; i++){
TreeNode node = queue.removeFirst();
list.add(node.val);//1
if(node.left != null) queue.addLast(node.left);//2
if(node.right != null) queue.addLast(node.right);//3
}
ans.add(list);
}
return ans;
}
}
二叉树的最近公共祖先(难)
236. 二叉树的最近公共祖先
吊题做一次忘一次!!!
首先想到的是自底向上地遍历。用布尔值F(x)表示x节点是p或q的祖先,那么符合题意的节点x必定满足:
(F(x_left) && F(x_right)) || ((x = p || x = q) && (F(x_left) || F(x_right)))
F(x_left) && F(x_right) 说明左子树和右子树均包含 p节点或q节点,如果左子树包含的是p节点,那么右子树只能包含 q节点,反之亦然,因为 p 节点和 q 节点都是不同且唯一的节点,如果满足这个判断条件即可说明 x 就是我们要找的最近公共祖先。
(x = p || x = q) && (F(x_left) || F(x_right)),这个判断条件即是考虑了 x 恰好是 p 节点或 q 节点且它的左子树或右子树有一个包含了另一个节点的情况,因此如果满足这个判断条件亦可说明 xx 就是我们要找的最近公共祖先。
这样找出来的公共祖先深度就是最大的,因为我们是自底向上从叶子节点开始更新的,所以在所有满足条件的公共祖先中一定是深度最大的祖先先被访问到。
具体做法,另外定义一个辅助函数,其实也就是深度优先遍历的函数,这样才能保证自底向上来。定义全局变量记录要返回的结果。那么当满足上述条件时,即可给其赋值。可以想象,一旦找到了这样的结果,即使后面还有很多满足条件的节点,都不会满足上述条件。因为找到在找到最近公共祖先x 以后,F(x)按定义被设置为 true ,即假定了这个子树中只有一个 p 节点或 q 节点,因此其他公共祖先不会再被判断为true。
class Solution {
TreeNode ans = null;//全局变量记录结果
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root, p, q);
return ans;
}
public boolean dfs(TreeNode root, TreeNode p, TreeNode q){//深度优先 自底向上
if(root == null) return false;
boolean leftNode = dfs(root.left, p, q);//左子树是p或q的祖先吗
boolean rightNode = dfs(root.right, p, q);//右子树是p或q的祖先吗
//如果满足上述的复杂条件 找到了 赋给ans
if((leftNode && rightNode) || (root.val == p.val || root.val == q.val) && (leftNode || rightNode)){
ans = root;
}
//当前节点的左/右子数是p/q的足下 或 当前节点是p/q?是的话返回true
return leftNode || rightNode || root.val == p.val || root.val == q.val;
}
}
二叉搜索树的最近公共祖先
235. 二叉搜索树的最近公共祖先
首先这个吊题用上面的做法肯定ok,但是你没用搜索树的性质就很拉胯。
在有序树中,判断⼀个节点的左子树⾥有p,右子树⾥有q,其实只要从上到下遍历的时候, 当前节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(true){
if(root.val < p.val && root.val < q.val){
root = root.right;
}else if(root.val > p.val && root.val > q.val){
root = root.left;
}else{
break;
}
}
return root;
}
}
很简单的做法,有一点值得深思:怎么保证到找到的就是最深的?
举个稍微复杂点的树看看就懂了。
这种方法,会一直去找【最贴近】p和q的结果。比如找4和5的,6满足,但不在[4,5]之间;同理2也是。二叉搜索树强大的特性使其有很多可以取巧的地方。
翻转二叉树
226. 翻转二叉树
剑指 Offer 27. 二叉树的镜像
看似很复杂,实际上想想,是不是把每个节点的左右子树都换一下顺序?就欧克啦。想到这就很轻松了,先来个递归三部曲:确定递归函数参数与返回值,确定终止条件,确定单层递归逻辑。
class Solution {
public TreeNode invertTree(TreeNode root) {//递归函数参数与返回值
if(root == null) return root;//终止条件
//单层递归逻辑
TreeNode tmp = root.left;//这里两者交互 要借助第三方辅助节点
root.left = invertTree(root.right);
root.right = invertTree(tmp);
return root;
}
}
现学现用,把层序遍历拿来。每次遍历一行节点时,把他们的左右子树交换就行。
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.addLast(root);//第一行肯定只有根节点
TreeNode node = null;
TreeNode tmp = null;
while(!queue.isEmpty()){//循环结束条件:队列空了
int n = queue.size();//一定要搞个变量存下当前队列(层)节点数量
for(int i = 0; i < n; i++){
node = queue.removeFirst();
//下面三行交换操作
tmp = node.left;
node.left = node.right;
node.right = tmp;
if(node.left != null) queue.addLast(node.left);
if(node.right != null) queue.addLast(node.right);
}
}
return root;
}
}
对称二叉树
101. 对称二叉树
剑指 Offer 28. 对称的二叉树
昨天刚看到一篇面经说手撕了这道题。其实得跟上面的镜像区分开,两道题的思想是不一样的!回到正题。一棵树满足对称,要满足三点:首先根节点的左右节点值要一样;其次根节点的左子节点的左子节点值 要等于 根节点的右子节点的右子节点值;最后根节点的左子节点的右子节点值 要等于 根节点的右子节点的左子节点值。
好好想一下上面这句话,有点绕。对着图看。那是不是说,如果所有节点都满足上述规律,整棵树就对称了?妙啊!
比较好想到的是递归,确实二叉树里递归太好用了。来递归三部曲:
- 确定方法参数和返回值:我们是要同时判断左右子树的外侧、内侧节点是否对应相等,所以参数应该是左右子树,返回值就是bool了;
- 确定终止条件:也就考虑下左右子树为空的处理方法;假如俩都为null,那就啥都没有,看做对称;否则只有一个为null的话,肯定不对称;
- 处理单层逻辑:判断当前树是否对称,三个条件:1.俩节点值相同,2.左的左子树与右的右子数对称,3.左的右子数和右的左子树对称。
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return recur(root.left, root.right);
}
boolean recur(TreeNode left_n, TreeNode right_n){
//假如俩都为null,那就啥都没有,看做对称;
if(left_n == null && right_n == null) return true;
//否则只有一个为null的话,肯定不对称
if(left_n == null || right_n == null) return false;
//三个条件:1.俩节点值相同,2.左的左子树与右的右子数对称,3.左的右子数和右的左子树对称。
return left_n.val == right_n.val && recur(left_n.left, right_n.right) && recur(left_n.right, right_n.left);
}
}
迭代法的逻辑有点繁琐,想看的可以去看Carl的pdf文档。
(你是不是在想:我才不看呢)
100. 相同的树
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q == null) return true;
if(p == null || q == null) return false;
return p.val == q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
class Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null) return false;
return isSameTree(root, subRoot) || isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q == null) return true;
if(p == null || q == null) return false;
return p.val == q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
判断是否为子树
剑指 Offer 26. 树的子结构
递归。先要找到节点值与子树根节点相同的节点,然后递归的判断左右子树是否都满足。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(A == null || B == null) return false;
return recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
public boolean recur(TreeNode A, TreeNode B){
if(B == null) return true;
if(A == null || A.val != B.val) return false;
else{
return recur(A.left, B.left) && recur(A.right, B.right);
}
}
}
二叉树的最大深度
104. 二叉树的最大深度
拿到题先分析:要求最深,可不就是root的左子树深度和右子数深度的较大者,加一吗?
然后左子树深度的不就是左子树的左子树深度和右子数深度的较大者,加一吗?
为啥加一,你往下的一层 不就是加一吗?本质上是DFS
太简单了,直接递归:
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
再写个三部曲吧:
- 确定递归函数返回值和参数:显然返回最大深度,参数只有root了
- 终止条件:一般都是遇到空节点了,他的深度为0
- 单层逻辑:找出左右子树的深度较大值,加一
二叉树的最小深度
111. 二叉树的最小深度
乍看之下和上面那题差不多,其实还是一点区别的。
要求最小深度,可不就是root的左子树深度和右子数深度的较小者,加一吗?
然后左子树最小深度的不就是左子树的左子树深度和右子数深度的较小者,加一吗?
为啥加一,你往下的一层 不就是加一吗?本质上是DFS
但是这里要注意,如果数的某个子树为空,那这个子树的深度应该是不存在,而不是0! 因为题目里对深度的定义是最小深度是从根节点到最近叶子节点的最短路径上的节点数量。没有叶子节点,谈何深度呢?
处理方法。判断以下有没有子树为空(或者说叶子节点是不是不全),有的话,当前节点的深度应该是另一个子树深度+1.
递归三部曲:
- 确定递归函数返回值和参数:深度,根节点
- 确定终止条件:if(root == null) return 0;
- 单层逻辑处理:若左子树为空,返回右子数最小深度+1;若右子树为空,返回左子数最小深度+1;都不为空,就返回较小者+1。
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
if(root.left == null) return minDepth(root.right) + 1;
if(root.right == null) return minDepth(root.left) + 1;
return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
}
}
如果面试官说不满意递归,再写个跌代,就记住要么DFS+栈,要么层序遍历+队列。这道题如果层序遍历,是不是一旦找到一个叶子节点,直接返回深度?妙啊。
相比递归,迭代好很多啊。
class Solution {
public int minDepth(TreeNode root) {
LinkedList<TreeNode> queue = new LinkedList<>();
queue.addLast(root);
int depth = 0;
if(root == null) return depth;
while(!queue.isEmpty()){
int n = queue.size();
depth++;
for(int i = 0; i < n; i++){
TreeNode node = queue.removeFirst();
if(node.left == null && node.right == null) return depth;
if(node.left != null) queue.addLast(node.left);
if(node.right != null) queue.addLast(node.right);
}
}
return depth;
}
}
完全二叉树的节点个数
222. 完全二叉树的节点个数
我首先想到的是层序遍历一个个数,但是时间空间复杂度都很高了。
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.addLast(root);
int ans = 0;
while(!queue.isEmpty()){
int n = queue.size();
for(int i=0; i < n; i++){
TreeNode node = queue.removeFirst();
ans++;
if(node.left != null) queue.addLast(node.left);
if(node.right != null) queue.addLast(node.right);
}
}
return ans;
}
}
模仿最大深度的问题,其实节点数等于左子树的节点数+右子数的节点数+1。这样几乎双百
递归很明显了:
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
等等,还没用到完全二叉树的性质呢。
完全⼆叉树只有两种情况,情况⼀:就是满⼆叉树,情况⼆:最后⼀层叶⼦节点没有满。对于情况⼀,可以直接⽤ 2^树深度 - 1 来计算,注意这⾥根节点深度为1。对于情况⼆,分别递归左孩⼦,和右孩⼦,递归到某⼀深度⼀定会有左孩⼦或者右孩⼦为满⼆叉树,然后依然可以按照情况1来计算。
思想很妙,代码更妙。细品吧
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
TreeNode l = root.left;
TreeNode r = root.right;
int leftH = 1;
int rightH = 1;
//下面两个循环很关键,判断树是不是满二叉树,
//就看左到头时的深度和右到头时的深度是不是一样
//当然这么写也是因为本题是个完全二叉树比较特殊
while(l != null){
l = l.left;
leftH++;
}
while(r != null){
r = r.right;
rightH++;
}
//如果是满,可以计算2^n-1
if(leftH == rightH){
return (int)Math.pow(2, leftH) - 1;
}
//否则递归左右子树
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
平衡二叉树
110. 平衡二叉树
说实话整理这道题的时候我又不会做了。
用刚刚写的求最大深度和最小深度的方法:
要是平衡树,三个条件:首先左子树和右子树高度差不大于1,其次左右子树分别也要是平衡树。那么我可以从上到下检查,显然这样会重复,但是简单。
class Solution {
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
//这句有点长 好好看看很核心
return Math.abs(getHeight(root.left) - getHeight(root.right)) > 1 ? false : isBalanced(root.left)&&isBalanced(root.right);
}
//这个刚学过
int getHeight(TreeNode root){
if(root == null) return 0;
return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}
}
那能不能从下往上呢?(一定要会下面的,面试官不喜欢上面)
自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 -1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。
class Solution {
public boolean isBalanced(TreeNode root) {
return height(root) >= 0;//其实就是他不等于-1就是平衡树
}
public int height(TreeNode root) {
if (root == null) {
return 0;
}
//一定要用两个变量把左右子树的高度存起来,不然后面会重复算好多遍!!!
int leftHeight = height(root.left);
int rightHeight = height(root.right);
//左/右子树不是平衡树 或 当前树不是平衡树,就标记当前节点-1
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
return -1;
} else {
return Math.max(leftHeight, rightHeight) + 1;
}
}
}
一定要用两个变量把左右子树的高度存起来,不然后面会重复算好多遍!!!
二叉树的所有路径
像是回溯,可是想不通回溯的写法,背
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> paths = new ArrayList<String>();
constructPaths(root, "", paths);
return paths;
}
public void constructPaths(TreeNode root, String path, List<String> paths) {
if (root != null) {
StringBuffer pathSB = new StringBuffer(path);//拷贝一份
pathSB.append(Integer.toString(root.val));
if (root.left == null && root.right == null) { // 当前节点是叶子节点
paths.add(pathSB.toString()); // 把路径加入到答案中
} else {
pathSB.append("->"); // 当前节点不是叶子节点,继续递归遍历
constructPaths(root.left, pathSB.toString(), paths);
constructPaths(root.right, pathSB.toString(), paths);
}
}
}
}
public class Solution {
ArrayList<ArrayList<Integer>> ans = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public ArrayList<ArrayList<Integer>> pathSum (TreeNode root, int sum) {
// write code here
dfs(root, 0, sum);
return ans;
}
void dfs(TreeNode root, int cnt, int sum){
if(root == null) return;
path.add(root.val);
cnt += root.val;
if(root.left == null && root.right == null){
if(cnt == sum) ans.add(new ArrayList(path));
}else{
dfs(root.left, cnt, sum);
dfs(root.right, cnt, sum);
}
path.remove(path.size() - 1);
}
}
二叉树的左叶子之和
404. 左叶子之和
DFS。如果当前节点的左子节点不是叶子,那就深度优先遍历下去,返回左右子树各自的结果之和。否则返回左叶子加上右子树的结果。
使用递归做二叉树的题时,想明白目标怎么由当前左右子树组成。
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) return 0;
if(root.left != null && root.left.left == null && root.left.right == null){
return root.left.val + sumOfLeftLeaves(root.right);
}else{
return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}
}
}
找树左下角的值
513. 找树左下角的值
先想到用层序遍历,方法简答但是应该挺慢的。
class Solution {
public int findBottomLeftValue(TreeNode root) {
LinkedList<TreeNode> queue = new LinkedList<>();
queue.addLast(root);
int ans = 0;
while(!queue.isEmpty()){
int n = queue.size();
List<Integer> path = new ArrayList<>();
for(int i = 0; i < n; i++){
TreeNode node = queue.removeFirst();
path.add(node.val);
if(node.left != null) queue.addLast(node.left);
if(node.right != null) queue.addLast(node.right);
}
ans = path.get(0);
}
return ans;
}
}
接下来是递归做法,很棒,要会写。
核心思想,dfs,我要找到的那个节点肯定要深度最大,的第一个。所以可以通过先序遍历的DFS来找最深的节点,同时维护一个最大深度值,每次找到新的最大深度,顺手把当前节点的值保存一下。
class Solution {
int maxV = -1;
int res = 0;
public int findBottomLeftValue(TreeNode root) {
dfs(root, 0);
return res;
}
void dfs(TreeNode root, int dep){
if(root == null) return;
dfs(root.left, dep+1);
if(dep > maxV){
maxV = dep;
res = root.val;
}
dfs(root.right, dep+1);
}
}
路径总和
如果需要搜索 整棵 二叉树,那么递归函数就不要返回值
如果要搜索 其中一条 符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。
递归三部曲
- 确定返回值和参数:需要⼆叉树的根节点,还需要⼀个计数器,这个计数器⽤来计算⼆叉树的⼀条边之和是否正好是⽬标和。
- 终止条件:不要去累加然后判断是否等于⽬标和,那么代码⽐较麻烦,可以⽤递减,让计数器count初始为⽬标和,然后每次减去遍历路径节点上的数值。如果最后count == 0,同时到了叶⼦节点的话,说明找到了⽬标和。如果遍历到了叶⼦节点, count不为0,就是没找到。
- 逻辑:因为终⽌条件是判断叶⼦节点,所以递归的过程中就不要让空节点进⼊递归了。递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该⽴刻返回。
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root == null) return false;
if(root.left == null && root.right == null && root.val == targetSum) return true;
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
}
113. 路径总和 II
这里其实不要一样,要求遍历整棵树,递归函数用void。有点回溯那味了。
但是这里要注意,有个地方(下面注释那里)千万不能找到一个直接return,因为树结构很特殊,他在dfs的时候自动的做到了不会重复。
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
recur(root, targetSum);
return ans;
}
public void recur(TreeNode root, int target){
if(root == null) return;
path.add(root.val);
target -= root.val;
if(root.left == null && root.right == null && target == 0){
ans.add(new ArrayList(path));//这里不要return
}
recur(root.left, target);
recur(root.right, target);
path.remove(path.size() - 1);
}
}
重构二叉树
105. 从前序与中序遍历序列构造二叉树
思路:先用map存一下中序结果的值与对应的索引下标。
然后用递归来构造 树,每次递归构造局部的一棵树。
- 递归的返回值就是节点,需要的参数:本次构造的先序遍历范围和中序遍历的范围(数组及左右界)
- 终止条件,两个数组的左右边界不对了(左大于右),返回空
- 单层逻辑,先把当前节点的值取出来(先序遍历的首位),然后递归构造左右子树。
核心在于每次把当前的节点找出来,然后对应的左子树的范围和右子树的范围。
class Solution {
Map<Integer, Integer> map;
public TreeNode buildTree(int[] preorder, int[] inorder) {
map = new HashMap<>();
int n = inorder.length;
for(int i = 0; i < n; i++){
map.put(inorder[i], i);
}
return recur(preorder, inorder, 0, n-1, 0, n-1);
}
TreeNode recur(int[] preorder, int[] inorder, int p_l, int p_r, int i_l, int i_r){
if(p_l > p_r || i_l > i_r) return null;
TreeNode root = new TreeNode(preorder[p_l]);
int mid = map.get(preorder[p_l]);
int left_len = mid - i_l;
//int right_len = i_r - mid;
root.left = recur(preorder, inorder, p_l + 1, p_l + left_len, i_l, mid - 1);
root.right = recur(preorder, inorder, p_l + left_len + 1, p_r, mid + 1, i_r);
return root;
}
}
class Solution {
Map<Integer, Integer> map;
public TreeNode buildTree(int[] inorder, int[] postorder) {
map = new HashMap<>();
int n = inorder.length;
for(int i = 0; i < n; i++){
map.put(inorder[i], i);
}
return recur(inorder, postorder, 0, n-1, 0, n-1);
}
public TreeNode recur(int[] inorder, int[] postorder, int i_l, int i_r, int p_l, int p_r){
if(i_l > i_r || p_l > p_r) return null;
TreeNode root = new TreeNode(postorder[p_r]);
int mid = map.get(postorder[p_r]);
int left_len = mid - i_l;
// int right_len = i_r - mid;
root.left = recur(inorder, postorder, i_l, mid - 1, p_l , p_l + left_len-1);
root.right = recur(inorder, postorder, mid+1, i_r, p_l + left_len, p_r-1);
return root;
}
}
最大二叉树
654. 最大二叉树
递归方式跟前面两题类似,主要就是每次要找到最大值的索引,作为左右子树的分界点。
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return recur(nums, 0, nums.length - 1);
}
public TreeNode recur(int[] nums, int le, int r) {
if(le > r) return null;
int idx = le;
for(int i = le + 1; i <= r; i++){
idx = nums[i] > nums[idx] ? i : idx;
}
TreeNode root = new TreeNode(nums[idx]);
root.left = recur(nums, le, idx - 1);
root.right = recur(nums, idx + 1, r);
return root;
}
}
合并二叉树
617. 合并二叉树
都为空的话,返回空
只有一个为空的话,返回那个不为空的
都不为空的话,把2树的值加到1树上去,然后递归左右子树。
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null && root2 == null) return null;
else if(root1 == null || root2 == null) return root1 == null ? root2 : root1;
else{
root1.val += root2.val;
root1.left = mergeTrees(root1.left, root2.left);
root1.right = mergeTrees(root1.right, root2.right);
}
return root1;
}
}
验证二叉搜索树
98. 验证二叉搜索树
递归函数 recur(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r) 的范围内(开区间)。如果 root 节点的值 val 不在 (l,r)的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 recur(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 recur(root.right, root.val, upper)。
函数递归调用的入口为 recur(root, -inf, +inf), inf 表示一个无穷大的值。
class Solution {
public boolean isValidBST(TreeNode root) {
return recur(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
boolean recur(TreeNode root, long low, long high){
if(root == null) return true;
if(root.val >= high || root.val <= low) return false;
return recur(root.left, low, root.val) && recur(root.right, root.val, high);
}
}
二叉搜索树的最小绝对差
530. 二叉搜索树的最小绝对差
搜索树一定要记得可以和中序遍历结合使用!!!
先中序遍历,再去找数组的最大间隙
class Solution {
List<Integer> list = new ArrayList<>();
public int getMinimumDifference(TreeNode root) {
inorder(root);
int min = list.get(1) - list.get(0);
for(int i = 2; i < list.size(); i++){
min = Math.min(min, list.get(i) - list.get(i-1));
}
return min;
}
void inorder(TreeNode root){
if(root == null) return;
inorder(root.left);
list.add(root.val);
inorder(root.right);
}
}
二叉搜索树中的众数
501. 二叉搜索树中的众数
先遍历,用哈希表统计每个值出现的次数,找到次数最大的是哪些值。
麻烦
class Solution {
Map<Integer, Integer> map = new HashMap<>();
public int[] findMode(TreeNode root) {
preorder(root);
int count = 0;
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
count = Math.max(count, entry.getValue());
}
List<Integer> list = new ArrayList<>();
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
if(entry.getValue() == count) list.add(entry.getKey());
}
int[] ans = new int[list.size()];
for(int i = 0; i < list.size(); i++){
ans[i] = list.get(i);
}
return ans;
}
void preorder(TreeNode root){
if(root == null) return;
map.put(root.val, map.getOrDefault(root.val, 0) + 1);
preorder(root.left);
preorder(root.right);
}
}