LeetCode刷题总结-树篇(上)
引子:刷题的过程可能是枯燥的,但程序员们的日常确不乏趣味。分享一则LeetCode上名为《打家劫舍 |||》题目的评论:
如有兴趣可以从此题为起点,去LeetCode开启刷题之旅,哈哈。该题目是选择一颗二叉树中对应节点的问题,也是本文收录的一道例题(具体请参考例12)。
本文开始分享作者对于LeetCode上有关树的刷题总结。谈到树,很多初学者会感觉很头疼。头疼的重点是其很多解法都离不开递归(或者说是深度优先搜索)的应用。而递归的难点在于其有很多返回值,对于这些返回值的顺序很难理顺,即代码虽短,但理解很烧脑。因此,对递归思想理解不够深的同学,建议先看作者的另一篇文章《LeetCode刷题总结-递归篇》,然后再开启攻克有关树的相关习题之旅(PS:这样会起到事半功倍的效果噢)。
在LeetCode的标签分类题库中,和树有关的标签有:树(123道题)、字典树(17道题)、线段树(11道题)、树状数组(6道题)。对于这些题,作者在粗略刷过一遍后,对其中的考点进行了总结,并归纳为以下四大类:
-
-
- 树的自身特性
- 树的类型
- 子树问题
- 新概念定义问题
-
对于上述四类考点,作者通过分析对比同类型考点的题目,选取其中比较经典或者有代表性的题目作为例题(共计收录约45道题)。在减少题量的同时,也希望能够全面覆盖LeetCode上关于树的相关习题的考点。作者计划分为三篇文章来讲解,本文是该系列的上篇,讲解考察树的自身特性相关考点的习题。选取的例题共21道,其中简单题5道、中等题13道、困难题3道。
关于树的自身特性总结归纳为四个问题:基本特性问题、构造问题、节点问题和路径问题,具体如下图所示。
树基本特性问题:请参考下文例1至例8。
树的构造问题:请参考下文例9、例10。
树的节点问题:请参考下文例11至例16。
树的路径问题:请参考下文例17至例21。
对于上述四个问题,基本特性和构造问题只需刷过一遍即可理解相关解法。对于树的节点和路径问题,则是本文例题中的相对困难的习题,一般需要重复刷或者深度分析和琢磨,才能感悟普适解法的套路。其中,在有关树的路径问题中,本文未收录树的前、中、后和层次遍历问题的习题,这些题目默认为较为基础的习题。
例1 对称二叉树
题号:101,难度:简单
题目描述:
解题思路:
递归思想的一个简单应用,从以树的根节点的左右子节点为根开始进行深度优先搜索,依次判断两颗子树的左子树是否更与其右子树,右子树是否等于其左子树即可。如果采用迭代则只需使用层次遍历,判断每层元素是否满足镜像对称即可。
具体代码:
class Solution { public boolean isSymmetric(TreeNode root) { if(root == null) return true; return dfs(root.left, root.right); } public boolean dfs(TreeNode left, TreeNode right) { if(left == null && right == null) return true; if(left == null || right == null || left.val != right.val) return false; return dfs(left.left, right.right) && dfs(left.right, right.left); } }
运行结果:
例2 翻转二叉树以匹配前序遍历
题号:971,难度:中等(关于翻转类习题,还可以参考题号226和951)
题目描述:
解题思路:
该题也是递归思想的应用。按照题目要求进行前序遍历,一旦遇到对应值与目标数组结果不同时,翻转遍历,接着继续遍历,如果最终结果依然不匹配则返回false,否则返回true。
具体代码:
class Solution { private int index; private int[] voyage; private List<Integer> result; public List<Integer> flipMatchVoyage(TreeNode root, int[] voyage) { // index = 0; this.voyage = voyage; result = new ArrayList<>(); dfs(root); // System.out.println("result = "+result); if(result.size() > 0 && result.get(result.size()-1) == -1) return new ArrayList<Integer>(Arrays.asList(-1)); return result; } public void dfs(TreeNode root) { if(root == null) return; if(root.val != voyage[index++]) result.add(-1); else { if(root.left != null && root.left.val != voyage[index]) { result.add(root.val); dfs(root.right); dfs(root.left); } else { dfs(root.left); dfs(root.right); } } } }
运行结果:
例3 输出二叉树
题号:655,难度:中等
题目描述:
解题思路:
此题是要求以二维数组的形式画出给定的二叉树。需要建立一个以根节点为原点的平面直角坐标系,然后依据广度优先搜索(即层次遍历)的思想依次初始化每层数组中元素的值即可,其中应用到了二分查找来确定每个元素的具体坐标,能够有效降低检索时间。
具体代码:
class Solution { public List<List<String>> printTree(TreeNode root) { List<List<String>> result = new ArrayList<>(); int dep = getDepth(root); Queue<TreeNode> queue = new LinkedList<>(); queue.add(root); // System.out.println("dep = "+dep); for(int i = 0;i < dep;i++) { List<String> list = new ArrayList<>(); for(int j = 0;j < Math.pow(2, dep)-1;j++) list.add(""); List<Integer> index = new ArrayList<>(); getIndex(i, 0, list.size() - 1, index); for(int j = 0;j < Math.pow(2, i);j++) { TreeNode temp = queue.poll(); if(temp == null) { queue.add(temp); queue.add(temp); } else { list.set(index.get(j), ""+temp.val); queue.add(temp.left); queue.add(temp.right); } } result.add(list); } return result; } public int getDepth(TreeNode root) { if(root == null) return 0; return 1 + Math.max(getDepth(root.left), getDepth(root.right)); } public void getIndex(int num, int left, int right, List<Integer> index) { int mid = (left + right) / 2; if(num == 0) index.add(mid); else { getIndex(num - 1, left, mid - 1, index); getIndex(num - 1, mid + 1, right, index); } } }
运行结果:
例4 合并二叉树
题号:617,难度:简单
题目描述:
解题思路:
此题比较简单,选取其中一个根节点作为返回值的根节点。然后应用深度优先搜索的思想,采用相同顺序同时遍历两棵树,如果当前节点均存在则相加,否则则选取含有值的节点。
具体代码:
class Solution { public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { if(t1 == null) return t2; else if(t2 == null) return t1; t1.left = mergeTrees(t1.left, t2.left); t1.right = mergeTrees(t1.right, t2.right); t1.val = t1.val + t2.val; return t1; } }
运行结果:
例5 二叉树剪枝
题号:814,难度:中等(另外,还可以参考题号669,修剪二叉搜索树)
题目描述:
解题思路:
此题属于二叉树节点删除问题的实际应用,并且结合深度优先搜索(前序遍历的应用)和回溯的思想。具体实现过程请参考下方代码。
具体代码:
class Solution { public TreeNode pruneTree(TreeNode root) { if(root == null) return root; if(root.val == 0 && root.left == null && root.right == null) root = root.left; else { root.left = pruneTree(root.left); root.right = pruneTree(root.right); } if(root != null && root.val == 0 && root.left == null && root.right == null) root = root.left; return root; } }
运行结果:
例6 二叉树的右视图
题号:199,难度:中等
题目描述:
解题思路:
层次遍历的实际应用。只需依次保存每层最右边的一个节点即可。
具体代码:
class Solution { public List<Integer> rightSideView(TreeNode root) { if(root == null) return new ArrayList<Integer>(); Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); List<Integer> result = new ArrayList<>(); while(queue.size() > 0) { int count = queue.size(); while(count-- > 0) { TreeNode temp = queue.poll(); if(count == 0) result.add(temp.val); if(temp.left != null) queue.offer(temp.left); if(temp.right != null) queue.offer(temp.right); } } return result; } }
运行结果:
例7 二叉树的最小深度
题号:111,难度:简单(最大深度请参考题号:104)
题目描述:
解题思路:
深度优先搜索的应用,代码很简洁,这个思想可以借鉴。
具体代码:
class Solution { public int minDepth(TreeNode root) { if(root == null) return 0; if(root.left != null && root.right != null) return 1 + Math.min(minDepth(root.left), minDepth(root.right)); else return 1 + minDepth(root.right) + minDepth(root.left); } }
运行结果:
例8 二叉树的最大宽度
题号:662,难度:中等(另外,可参考题号:543,二叉树的直径)
题目描述:
解题思路:
层次遍历的实际应用,依次更新每层最大宽度即可。
具体代码:
class Solution { public int widthOfBinaryTree(TreeNode root) { if(root == null) return 0; int result = 0; Queue<TreeNode> queue = new LinkedList<>(); Queue<Integer> index = new LinkedList<>(); queue.offer(root); index.offer(1); while(queue.size() > 0) { int count = queue.size(); int left = index.peek(); // System.out.println("left = "+left+", count = "+count); while(count-- > 0) { TreeNode temp = queue.poll(); int i = index.poll(); if(temp.left != null) { queue.offer(temp.left); index.offer(i * 2); } if(temp.right != null) { queue.offer(temp.right); index.offer(i * 2 + 1); } if(count == 0) result = Math.max(result, 1 + i - left); } } return result; } }
运行结果:
例9 依据前序和后序遍历构造二叉树
题号:889,难度:中等(另外,可参考同类型习题,题号:105,106,1008)
题目描述:
解题思路:
可以先手动构造画以下,体会其中的构造规则,然后采用深度优先搜索的思想来实现。每次找到当前子树的根节点,并确定左右子树的长度,并不断递归遍历构造即可。
具体代码:
class Solution { private int[] pre; private int[] post; private Map<Integer, Integer> map; public TreeNode constructFromPrePost(int[] pre, int[] post) { this.pre = pre; this.post = post; map = new HashMap<>(); for(int i = 0;i < post.length;i++) map.put(post[i], i); return dfs(0, pre.length-1, 0, post.length-1); } public TreeNode dfs(int pre_left, int pre_right, int post_left, int post_right) { if(pre_left > pre_right || post_left > post_right) return null; TreeNode root = new TreeNode(pre[pre_left]); int len = 0; if(pre_left + 1 < pre_right) len = map.get(pre[pre_left+1]) - post_left; root.left = dfs(pre_left+1, pre_left+1+len < pre_right ? pre_left+1+len: pre_right, post_left, post_left+len); root.right = dfs(pre_left+len+2, pre_right, post_left+len+1, post_right-1); return root; } }
运行结果:
例10 从先序遍历还原二叉树
题号:1028,难度:困难
题目描述:
解题思路:
定义一个全局变量用于确定当前深度优先遍历元素处在左子树还是右子树,能够有效减少代码量,并提高代码的可阅读性。
具体代码:
class Solution { int i = 0; // 神来之笔, 定义全局变量i,可以有效区分左子树和右子树 public TreeNode recoverFromPreorder(String s) { return buildtree(s,0); } public TreeNode buildtree(String s,int depth){ if(i == s.length()) return null; TreeNode cur = null; int begin = i; while(s.charAt(begin) == '-') begin ++; int end = begin; while(end < s.length() && s.charAt(end) - '0' >= 0 && s.charAt(end) - '0' < 10) end ++; if(begin - i == depth){ cur = new TreeNode(Integer.valueOf(s.substring(begin,end))); i = end; } if(cur != null){ // System.out.println("dep = "+depth+", cur = "+cur.val); cur.left = buildtree(s,depth + 1); cur.right = buildtree(s,depth + 1); // 通过全局变量i,可以在同一层深度找到右子树 } return cur; } }
运行结果:
例11 二叉树的最近公共祖先
题号:236,难度:中等
题目描述:
解题思路:
此题一道和经典的面试题,代码量很少,但是对于很多初学者来说比较难以理解。采用深度优先搜索的思想,搜索目标节点。具体解题思路请参考代码。
具体代码:
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { // LCA 问题 if (root == null) { return root; } if (root == p || root == q) { return root; } TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if (left != null && right != null) { return root; } else if (left != null) { return left; } else if (right != null) { return right; } return null; } }
运行结果:
例12 打家劫舍 III
题号:337,难度:中等
题目描述:
解题思路:
本题考察后序遍历思想的应用,感觉外加了一点动态规划的思维。题目要求是寻找一个想加和较大的节点集。具体实现思路请参考代码。
具体代码:
class Solution { public int rob(TreeNode root) { return postorder(root); } public int postorder(TreeNode root){ if(root == null) return 0; postorder(root.left); postorder(root.right); int res1 = 0; // 左右 int res2 = root.val; //根 if (root.left != null){ res1 += root.left.val; if (root.left.left != null) res2 += root.left.left.val; if (root.left.right != null) res2 += root.left.right.val; } if (root.right != null){ res1 += root.right.val; if (root.right.left != null) res2 += root.right.left.val; if (root.right.right!=null) res2 += root.right.right.val; } root.val = Math.max(res1, res2); return root.val; } }
运行结果:
例13 在二叉树中增加一行
题号:623,难度:中等
题目描述:
解题思路:
此题考察二叉树的添加节点的问题。并且保持原有节点的相对顺序不断,具体解题思路可参考代码。
具体代码:
class Solution { public TreeNode addOneRow(TreeNode root, int v, int d) { if (d == 0 || d == 1) { TreeNode t = new TreeNode(v); if (d == 1) t.left = root; else t.right = root; return t; } if (root != null && d > 1) { root.left = addOneRow(root.left, v, d > 2 ? d - 1 : 1); root.right = addOneRow(root.right, v, d > 2 ? d - 1 : 0); } return root; } }
运行结果:
例14 二叉树中所有距离为K的节点
题号:863,难度:中等
题目描述:
解题思路:
保存从根节点开始到叶子节点的每个路径,然后找到目标节点的位置,按照距离大小采用哈希定位的思想找到对应节点。
具体代码:
class Solution { private Map<TreeNode,String>map=new HashMap<>(); private String path; public List<Integer> distanceK(TreeNode root, TreeNode target, int K) { List<Integer>list=new ArrayList<>(); getNodeDist(root,target,""); int i; for(TreeNode key:map.keySet()){ String s=map.get(key); for(i=0;i<s.length()&&i<path.length()&&s.charAt(i)==path.charAt(i);i++); if(s.length()-i+path.length()-i==K) list.add(key.val); } return list; } public void getNodeDist(TreeNode root,TreeNode target,String p){ if(root != null){ path = root == target ? p : path; map.put(root, p); getNodeDist(root.left,target,p+"0"); getNodeDist(root.right,target,p+"1"); } } }
运行结果:
例15 监控二叉树
题号:968,难度:困难
题目描述:
解题思路:
此题也是选取一个符合题目要求的节点子集,但是取的要求是间隔化取点,并且需要满足数量最小。具体实现可参考下方代码。
具体代码:
class Solution { private int ans = 0; public int minCameraCover(TreeNode root) { if (root == null) return 0; if (dfs(root) == 2) ans++; return ans; } // 1:该节点安装了监视器 2:该节点可观,但没有安装监视器 3:该节点不可观 private int dfs(TreeNode node) { if (node == null) return 1; int left = dfs(node.left), right = dfs(node.right); if (left == 2 || right == 2) { ans++; return 0; } else if (left == 0 || right == 0){ return 1; } else return 2; } }
运行结果:
例16 二叉树着色游戏
题号:1145,难度:中等
题目描述:
解题思路:
此题也是一道节点选择的问题,但是涉及到了博弈论。按照题目的要求我们会发现选择一个节点后正常情况下会把整棵树分为三个部分,只需要获胜者能够访问的一部分节点个数大于另一方即可确保最终获胜。
具体代码:
class Solution { //极客1选的起始点有多少个左节点 private int left = 0; //极客1选的起始点有多少个右节点 private int right = 0; public boolean btreeGameWinningMove(TreeNode root, int n, int x) { //极客1选了第一个节点后,将树划分为了三个部分(可能为空) //第一部分:left 第二部分:right 第三部分:n - (left + right) - 1 //只需要总结点的数的一半 < 三个部分中的最大值,极客2就可以获胜 return getNum(root, x) / 2 < Math.max(Math.max(left, right), n - (left + right) - 1); } private int getNum(TreeNode node, int x) { if (node == null) { return 0; } int r = getNum(node.right, x); int l = getNum(node.left, x); if (node.val == x) { left = l; right = r; } return l + r + 1; } }
运行结果:
例17 二叉树的所有路径
题号:257,难度:简单
题目描述:
解题思路:
此题是路径选择的一个基本习题,是解决路径相关问题的必须掌握的一道题。采用深度优先搜索保存每条路径即可。
具体代码:
class Solution { public List<String> binaryTreePaths(TreeNode root) { List<String> ret = new ArrayList<>(); if(root==null) return ret; solve(root, "", ret); return ret; } public void solve(TreeNode root, String cur, List<String> ret){ if(root==null) return; cur += root.val; if(root.left == null && root.right == null) { ret.add(cur); } else { solve(root.left, cur+"->", ret); solve(root.right, cur+"->", ret); } } }
运行结果:
例18 二叉树中分配硬币
题号:979,难度:中等
题目描述:
解题思路:
本题考察我们采用前序遍历,并抽象为本题解答的过程。具体原理请参考代码。
具体代码:
class Solution { /** * 从后序遍历的第一个叶子节点开始,假设自己有x个金币,剩余x-1个金币都还给父节点,x-1可能为负数、0、正数 * x-1 < 0说明不够金币,需要从父节点获得,因此子节点有|x-1|个入方向的操作,次数加上|x-1| * x-1 == 0说明刚好,无需与父节点有金币的交换,次数加0 * x-1 > 0 说明有多余的金币,需要交给父节点,因此子节点有x-1个出方向的操作,次数加上|x-1| */ private int ans = 0;// 移动次数 public int distributeCoins(TreeNode root) { lrd(root); return ans; } public int lrd(TreeNode root){ if(root == null){ return 0; } if(root.left != null){ root.val += lrd(root.left); } if(root.right != null){ root.val += lrd(root.right); } ans += Math.abs(root.val - 1); return root.val - 1; } }
运行结果:
例19 二叉树的垂序遍历
题号:987,难度:中等
题目描述:
解题思路:
通过给每个节点定制编号的思路,采用前序遍历的思想来完成本题要求的垂序遍历。
具体代码:
class Solution { private Map<Integer, List<List<Integer>>> map = new HashMap<>(); private int depth; public List<List<Integer>> verticalTraversal(TreeNode root) { depth = getDepth(root); dfs(root, 0, 0); List<List<Integer>> result = new ArrayList<>(); int min = 0; for(Integer key: map.keySet()){ min = Math.min(min, key); result.add(new ArrayList<Integer>()); } for(Integer key: map.keySet()){ for(int i = 0;i < depth;i++) { List<Integer> temp = map.get(key).get(i); if(temp.size() == 1) result.get(key-min).add(temp.get(0)); else if(temp.size() > 1) { // 同层同列的元素,按照从小到大排序 Collections.sort(temp); for(Integer t: temp) result.get(key-min).add(t); } } } return result; } public int getDepth(TreeNode root) { if(root == null) return 0; return 1 + Math.max(getDepth(root.left), getDepth(root.right)); } public void dfs(TreeNode root, int x, int y) { if(root == null) return; List<List<Integer>> temp; if(map.containsKey(x)) temp = map.get(x); else { temp = new ArrayList<>(); for(int i = 0;i < depth;i++) temp.add(new ArrayList<Integer>()); } temp.get(y).add(root.val); map.put(x, temp); dfs(root.left, x-1, y+1); dfs(root.right, x+1, y+1); } }
运行结果:
例20 二叉树中的最大路径和
题号:124,难度:困难
题目描述:
解题思路:
这道题的解题思路和例11 二叉树的最近公共祖先比较相似,都是采用深度优先搜索的思想,并分别寻找左右子树的结果,最后和根节点进行比较。具体实现的思路请参考下方代码。
具体代码:
class Solution { private int ret = Integer.MIN_VALUE; public int maxPathSum(TreeNode root) { /** 对于任意一个节点, 如果最大和路径包含该节点, 那么只可能是两种情况: 1. 其左右子树中所构成的和路径值较大的那个加上该节点的值后向父节点回溯构成最大路径 2. 左右子树都在最大路径中, 加上该节点的值构成了最终的最大路径 **/ getMax(root); return ret; } private int getMax(TreeNode r) { if(r == null) return 0; int left = Math.max(0, getMax(r.left)); // 如果子树路径和为负则应当置0表示最大路径不包含子树 int right = Math.max(0, getMax(r.right)); ret = Math.max(ret, r.val + left + right); // 判断在该节点包含左右子树的路径和是否大于当前最大路径和 return Math.max(left, right) + r.val; } }
运行结果:
例21 路径总和 |||
题号:437,难度:简单
题目描述:
解题思路:
首先,此题并不简单。其次,本题是二叉树路径问题中一个很有代表性的问题。采用前序遍历的思想,以及根节点和子树的关系,不断更新最终结果。
具体代码:
class Solution { int pathnumber; public int pathSum(TreeNode root, int sum) { if(root == null) return 0; Sum(root,sum); pathSum(root.left,sum); pathSum(root.right,sum); return pathnumber; } public void Sum(TreeNode root, int sum){ if(root == null) return; sum-=root.val; if(sum == 0){ pathnumber++; } Sum(root.left,sum); Sum(root.right,sum); } }
运行结果: