引子:刷題的過程多是枯燥的,但程序員們的平常確不乏趣味。分享一則LeetCode上名爲《打家劫舍 |||》題目的評論:html
若有興趣能夠今後題爲起點,去LeetCode開啓刷題之旅,哈哈。該題目是選擇一顆二叉樹中對應節點的問題,也是本文收錄的一道例題(具體請參考例12)。node
本文開始分享做者對於LeetCode上有關樹的刷題總結。談到樹,不少初學者會感受很頭疼。頭疼的重點是其不少解法都離不開遞歸(或者說是深度優先搜索)的應用。而遞歸的難點在於其有不少返回值,對於這些返回值的順序很難理順,即代碼雖短,但理解很燒腦。所以,對遞歸思想理解不夠深的同窗,建議先看做者的另外一篇文章《LeetCode刷題總結-遞歸篇》,而後再開啓攻克有關樹的相關習題之旅(PS:這樣會起到事半功倍的效果噢)。程序員
在LeetCode的標籤分類題庫中,和樹有關的標籤有:樹(123道題)、字典樹(17道題)、線段樹(11道題)、樹狀數組(6道題)。對於這些題,做者在粗略刷過一遍後,對其中的考點進行了總結,並概括爲如下四大類:面試
對於上述四類考點,做者經過分析對比同類型考點的題目,選取其中比較經典或者有表明性的題目做爲例題(共計收錄約45道題)。在減小題量的同時,也但願可以全面覆蓋LeetCode上關於樹的相關習題的考點。做者計劃分爲三篇文章來說解,本文是該系列的上篇,講解考察樹的自身特性相關考點的習題。選取的例題共21道,其中簡單題5道、中等題13道、困難題3道。數組
關於樹的自身特性總結概括爲四個問題:基本特性問題、構造問題、節點問題和路徑問題,具體以下圖所示。ide
樹基本特性問題:請參考下文例1至例8。post
樹的構造問題:請參考下文例九、例10。ui
樹的節點問題:請參考下文例11至例16。this
樹的路徑問題:請參考下文例17至例21。spa
對於上述四個問題,基本特性和構造問題只需刷過一遍便可理解相關解法。對於樹的節點和路徑問題,則是本文例題中的相對困難的習題,通常須要重複刷或者深度分析和琢磨,才能感悟普適解法的套路。其中,在有關樹的路徑問題中,本文未收錄樹的前、中、後和層次遍歷問題的習題,這些題目默認爲較爲基礎的習題。
題號: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); } }
運行結果:
題號: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); } } } }
運行結果:
題號: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); } } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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); } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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"); } } }
運行結果:
題號: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; } }
運行結果:
題號: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; } }
運行結果:
題號: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); } } }
運行結果:
題號: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; } }
運行結果:
題號: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); } }
運行結果:
題號: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; } }
運行結果:
題號: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); } }
運行結果: