引子:刷題的過程可能是枯燥的,但程序員們的日常確不乏趣味。分享一則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); } }
運行結果: