[算法系列]遞歸應用——二叉樹(1):二叉樹遍歷詳解解+LeetCode經典題目+模板總結

本文是遞歸算法系列文的第7篇,依然沿着遞歸的脈絡,介紹了常常運用遞歸處理問題的一個典型數據結構——二叉樹。分析總結了LeetCode中的相關習題在解法上的思路和模板。

本文內容如下:

  • 樹的前、中、後序、層次遍歷的遞歸和非遞歸寫法
  • LeetCode上樹的問題分類(基本遍歷、路徑、計數、加和、深寬、構造、BST等)
  • 兩種遍歷爲思想的問題(判定、比較結點或子樹 以及 路徑[ 和 ]、累計
  • 【小結】在解決樹的遍歷相關問題時,我們是如何使用基本遍歷方法,進行遞歸設計的?

由於樹的相關問題題目較多,本文介紹第一部分,其餘部分後續更新。(又挖了一個坑)

0.LeetCode中二叉樹定義

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}

1.四種遍歷(遞歸+非遞歸)

1.1遞歸遍歷

LC144前序

    List<Integer> res = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) return res;

        res.add(root.val);
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return res;
    }

LC94中序

    List<Integer> res = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null)    return res;
        
        inorderTraversal(root.left);
        res.add(root.val);
        inorderTraversal(root.right);    
        return res;
    }

LC145後序

    List<Integer> res = new ArrayList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root == null) return res;

        postorderTraversal(root.left);
        postorderTraversal(root.right);
        res.add(root.val);
        return res;
    }

1.2非遞歸遍歷

前序

    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if(root == null) return res;

        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode p = root;

        while(!stack.isEmpty() || p != null){ 
            while(p!= null){	//一口氣走到左下最後一個,一邊走一邊入棧、同時加入結果集
                stack.push(p);          
                res.add(p.val);      
                p = p.left;
            }    
        
            p = stack.pop().right;	//逐個往上、然後遍歷右子樹
        }
        return res;
    }

注:前序還有一種寫法:這種寫法具有結構對稱美哦~後序就知道了

  • 根不空時,根入棧
  • 當棧非空時:
    • 根出棧,加入res。
    • 若右子樹非空,右子樹入棧
    • 若左子樹非空,左子樹入棧
    public List<Integer> preorderTraversal(TreeNode root) {        
		List<Integer> res = new ArrayList<>();        
        if(root == null) return res;

        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode p = root;
        stack.push(p);				//根節點入棧
        while(!stack.isEmpty()){	//當根節點不空時,出棧一個根節點,然後加入res中
            p = stack.pop();
            res.add(p.val);
            if(p.right != null)                //加入右子樹入棧
                stack.push(p.right);
            if(p.left != null)				//左子樹入棧
                stack.push(p.left);
        }
        return res;
    }        

中序

    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();    
        if(root == null)    return res;
        
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode p = root;

        while(!stack.isEmpty() || p != null){ //一口氣走到左下角一邊走,一邊入棧,
            while(p != null){				//但是是在出棧後才加到結果集
                stack.push(p);
                p = p.left;
            }

            p = stack.pop();
            res.add(p.val);
            p = p.right;
        }
        return res;
    }

後序

後序遍歷使用了一個小技巧:

  • 後序遍歷是右 =》左 =》 根, 那麼我們可以先按照根 =》左 =》右(前序)進行遍歷,然後將得到的結果進行翻轉
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();        
        if(root == null) return res;

        Deque<TreeNode> stack = new ArrayDeque<>();
        // Deque<TreeNode> temp_res = new ArrayDeque<>();
        TreeNode p = root;
        stack.push(p);
        while(!stack.isEmpty()){
            p = stack.pop();
            res.add(p.val);
            if(p.left != null)
                stack.push(p.left);
            if(p.right != null)                
                stack.push(p.right);
        }	
        Collections.reverse(res); //將結果翻轉,這一步也可以用棧
        return res;

    }

1.3層次遍歷

以LC102爲例:輸入二叉樹數組,輸出層次遍歷結果,並且每一層爲一個list,整體爲二維list

https://leetcode-cn.com/problems/binary-tree-level-order-traversal/

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        
        if(root == null)    return res;

        Deque<TreeNode> queue = new ArrayDeque<>();
        queue.offer(root);

        while(!queue.isEmpty()){
            List<Integer> item = new ArrayList<>();
            
            for(int i = queue.size() ;  i > 0 ; i --){
                TreeNode node = queue.poll();
                item.add(node.val);
                if(node.left!= null) queue.offer(node.left);
                if(node.right!= null) queue.offer(node.right);
            }
            res.add(item);
 
        }
        return res;
    }

2.常見問題分類彙總

LC中樹標籤下的前60來道題經過總結,從問題角度,大概如下包括類別:【標紅爲本次涉及內容,未標紅爲後續文章內容】

  • 遍歷樹進行比較(結點、子樹)

  • 深度、路徑(和)、計數

  • (修改)構造相關

    • 根據遍歷構造(前中構造,後中構造)
    • 前序後繼、鏈表轉化
    • AVL(這個與深度關係也大)
    • 翻轉、修改等構造
    • 序列化反序列化
  • 二叉搜索樹相關(可能會運用BST性質來解題)

  • 明顯帶有層次遍歷的暗示

    • 寬度、鋸齒遍歷、層平均值、最大值、左右視圖、右側指針
  • 順序(前繼後繼)

暫且大概將其分爲這些類別

其實這樣分類也不是太好,一是有些題的從解法層面有多種,二是有些雖然題雖然屬於這一類,但是解法更像另一類。。能力有限還請見諒

樹的遍歷(判定、比較子樹或節點間的關係)

LC100相同的樹

思路:

判斷樹相同等價於:對兩棵樹的每一對應節點左如下判斷:

  • 若p,q同時爲空,則爲true
  • 若一個空一個非空,則爲false
  • 若p、q兩個非空:
    • p.val 與 q.val 不等,則false
    • 若相等,對其左右子樹進行上述相同判斷(遞歸),且當左右子樹同時滿足時,返回true

    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p == null && q == null)  return true;
        if(p == null || q == null || p.val != q.val)  return false;

        return isSameTree(p.left , q.left) && isSameTree(p.right, q.right);
    }

LC101對稱二叉樹

    1
   / \
  2   2
 / \ / \
3  4 4  3

思路:按照對稱條件,對應節點相等(左子樹的左子樹與右子樹的右子樹、左子樹的右子樹與右子樹的左子樹)

  • 若根爲空直接返回true
  • 比較根的左子樹和右子樹(值是否相等)
    • 左右子樹同爲null ==> true
    • 左右子樹只有一個爲null,或者都不爲null但值不同 ==> false
    • 遞歸對:左子樹的左子樹與右子樹的右子樹、左子樹的右子樹與右子樹的左子樹 進行考察
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;

        return compare(root.left , root.right);
    }

    private boolean compare(TreeNode node1, TreeNode node2){
        if(node1 == null && node2 == null) return true;

        if(node1 == null || node2 == null || node1.val != node2.val)
            return false;
        
        return compare(node1.left , node2.right) && compare(node1.right , node2.left);
    }

LC110平衡二叉樹

思路1

平衡二叉樹 等價於下面兩個條件都滿足:

  • 左子樹高度與右子樹高度差爲1
  • 左右子樹也爲平衡二叉樹
    public boolean isBalanced(TreeNode root){
        if (root == null) return true;

        return Math.abs(depth(root.left) - depth(root.right))
         <=1   && isBalanced(root.left) && isBalanced(root.right);
    }

	//就是求高度
    private int depth(TreeNode node){
        if(node == null) return 0;
        if(node.left == null && node.right == null)
            return 1;
        
        return Math.max(depth(node.left) , depth(node.right)) +1;
    }

思路2:

  • 精妙之處在於用-1表示非平衡,0、1分別表示兩棵樹(爲平衡時)的高度差,大於1也表示非平衡
  • 向左遞歸到最左得到左邊結果left (表示的是以所有左子樹的判斷結果)
  • 向右遞歸到最右得到右邊結果right(表示的是以所有右子樹的判斷結果)
  • 返回結果:即考察左右兩子樹差值是否小於2
    public boolean isBalanced(TreeNode root){
        if(root == null) return true;

        return checkBalanced(root) != -1;
    }

    private int checkBalanced(TreeNode node){
        if(node == null) return 0;

        int left = checkBalanced(node.left);
        if(left == -1) return -1;
        int right = checkBalanced(node.right);
        if(right == -1) return -1;
        

        return Math.abs(left - right) < 2? Math.max(left , right) + 1: -1;
    }

572另一個樹的子樹

思路:

  • 遞歸比對s中所有結點與t的根
  • 當s某結點值等於t的根的值時,遞歸比較兩子樹
	boolean flag = false;	
	public boolean isSubtree(TreeNode s, TreeNode t) {
		if(s == null && t == null) return true;
		if(s == null || t == null) return  false;    
		if(s .val == t.val) flag  = verty(s, t);
		if(flag) return true;   //當前節點已經找到了,直接返回truetrue
		
        //當前節點沒找到,需要遞歸去左右節點找
		flag =  isSubtree(s.left , t) || isSubtree(s.right, t);

		return  flag;
	}

    //判斷以s和t兩個節點爲根的兩個樹是否全等
	private boolean verty(TreeNode s, TreeNode t) {
		if(s == null && t == null) return true; 
		if(s == null || t == null) return  false;
		
		if(s.val != t.val) return  false;
		
		return verty(s.left , t.left)  &&  verty(s.right , t.right);
	}

LC671二叉樹中第二小的節點

思路:通過題意可以看出,該二叉樹的最小值即爲根,因此我們只需要找到和根值不同的那個最小值即可,它可能在左子樹也可能在右子樹。

因此,設計遞歸函數helper(root, root.val)

分別在左子樹和右子樹中去尋找大於根的值的結點,返回二者中的較小值

    public int findSecondMinimumValue(TreeNode root) {
        if(root == null) return -1;
        return helper(root, root.val);
    }

    private int helper(TreeNode root , int min){
        if(root == null) return -1;
        if(root.val > min) return root.val;	//這裏表示找到了

        int left = helper(root.left , min);
        int right = helper(root.right , min);
        if(left == -1) return right;
        if(right == -1) return left;
        return Math.min(left , right);
    }

深度、路徑(和)、計數、加和

LC104.二叉樹的最大深度

思路:該樹深度即爲左子樹和右子樹深度大的那個值加1。 對於其左子樹和右子樹同樣如此,因此進行遞歸求解

    public int maxDepth(TreeNode root) {
    	if(root == null)	return 0;
    	
    	int left_height = maxDepth(root.left);
    	int right_height = maxDepth(root.right);
    	return Math.max(left_height, right_height)+1;
    }

LC111.二叉樹的最小深度

思路:二叉樹最小深度即爲左子樹和右子樹中深度小的那個

    public int minDepth(TreeNode root) {
        if(root == null) return 0;

        if(root.left == null && root.right == null)
            return 1;
        
        //注意此處,當左或右子樹邊沒有時,需要在另一邊加1
        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;
    }   

LC124二叉樹中的最大路徑和

思路:求從任意一個節點到另一個節點的最大路徑。我們需要在遞歸函數中需要做兩件事:

  1. 返回從該節點到下方某節點(也有可能就是自身不動)的路徑最大值
  2. 對該節點,比較更新 res 和(當前節點值+左路徑最大值 + 右路徑最大值)
 -10                     25 (34)
   / \  			  /        \
  9  20      ==》    9(9)      35 (35)    
    /  \			           /    \
   15  -7  			       15(15)  0(-7)
  • 對於9,15,-7葉子節點,其返回9,15,0 (負數返回0,表示上面的節點不會來這)
  • 對20節點來說,返回其與左孩子節點和35,根節點類似
  • 上圖中,無括號的表示每個位置處返回的最大往下的路徑; 括號中表示此時的最終結果(res)
    int res = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        dfs(root);
        return res;
    }

    private int dfs(TreeNode root){
        if(root == null)    return 0;

        int left = dfs(root.left);
        int right = dfs(root.right);

        res = Math.max(res , left + root.val + right);
        return  Math.max(0 , root.val + Math.max(left,right));
    }

LC129求根到葉子節點數字之和

思路:在進行遞歸遍歷時,保存一個curr,用於存放目前爲止的結果,因此在遞歸遍歷中,對每個結點

    int res ;
    public int sumNumbers(TreeNode root) {
        if(root == null)    return 0;
        dfs(root , 0);
        return res;
    }

    private void dfs(TreeNode node , int curr){
        if(node == null)    return;
        curr += node.val;
        if(node.left == null && node.right == null )
            res += curr;
        
        dfs(node.left , curr*10);
        dfs(node.right , curr*10);
    }

LC112路徑總和

思路:判斷在樹中存在一條和爲未指定值的路徑,遞歸遍歷,每次將sum減去當前val即可

    public boolean hasPathSum(TreeNode root, int sum) {
        if(root == null) return false;
        if(sum == root.val && root.left == null && root.right == null)
            return true;

        boolean left = false, right = false;
        //if(root.left != null ) 前面已經對root進行判空
            left =  hasPathSum(root.left , sum - root.val) ;
        //if(root.right != null)
            right = hasPathSum(root.right , sum - root.val);
        
        return left || right;
    }

LC113路徑之和2

    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> res = new ArrayList<>();
        if(root == null) return res;

        Deque<Integer> item = new ArrayDeque<>();
        dfs(root, sum , res ,item);
        return res;
    }
    private void dfs(TreeNode node , int sum ,  List<List<Integer>> res ,Deque<Integer>  item ){
        if(node == null ) return;

        item.add(node.val);
        if(sum == node.val && node.left == null && node.right == null){
            res.add(new ArrayList<Integer>(item));
            item.removeLast(); //回溯,返回上一個節點
            return;
        }

        dfs(node.left , sum - node.val  , res , item);
        dfs(node.right , sum - node.val , res, item);
        item.removeLast(); //回溯

    }

LC437路徑總和3

思路:該題在111的基礎上擴充了兩點:

  • 111從根出發,該題是從任意節點出發
    • 針對該問題,我們需要將每個節點視爲根進行遍歷一次。
  • 111到達葉子,該題可到任意下方節點
    • 結束條件不用再判斷是否爲葉子
    public int pathSum(TreeNode root, int sum) {
        if(root == null ) return 0;
        
        //求以該節點爲根的路徑個數
        int res = findPath(root, sum);

        //對每個節點,都作爲根進行尋找路徑   
        int left = pathSum(root.left , sum);
        int right = pathSum(root.right , sum);
        
         //最後返回:當前節點出發的路徑數+左子樹所有滿足的路徑+右子樹所有路徑
        return left + res + right;
    }
	
	//求以這個節點爲根的路徑個數的具體實現
    private int findPath(TreeNode node,  int sum){
        if(node == null) return 0;

        int count = sum == node.val ? 1 : 0; //若到達此處時,sum剛好還剩node.val,count計爲1
        return count + findPath(node.left ,sum - node.val) + findPath(node.right, sum -node.val); 
    }

LC257二叉樹的所有路徑

    public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new ArrayList<>();
        if(root == null) return res;

        dfs(root, res , new StringBuilder());
        return res;
    }

    private void dfs(TreeNode root, List<String> res ,StringBuilder stringBuilder){
        if(root == null ) return ;
        if(root.left == null && root.right == null){
            res.add(stringBuilder.toString() + root.val);
        }else{
            String temStr = root.val + "->";
            stringBuilder.append(temStr);
            dfs(root.left , res , stringBuilder);
            dfs(root.right , res ,stringBuilder);

            //回溯
            stringBuilder.delete(stringBuilder.length() - temStr.length(),
                stringBuilder.length());
        }
    }

LC687最長同值路徑

思路:此題和124,類似,都是在遞歸同時要幹兩件事:

  • 返回的是該節點作爲根時樹中的最長同值路徑
  • 比較
    int res;
    public int longestUnivaluePath(TreeNode root) {
        res = 0;
        getLength(root);
        return  res;
    }

    private int getLength(TreeNode root) {
        if(root == null) return 0;
        
        int left = getLength(root.left);//遞歸求解左子樹的結果
        int right = getLength(root.right); 
        
        int tem_left = 0 , tem_right = 0;
        if(root.left != null && root.val == root.left.val)
            tem_left += left+1; //左邊結果值爲左邊遞歸返回的結果加1
        if(root.right != null && root.val == root.right.val)
            tem_right += right +1;
        
        res = Math.max(res , tem_left +tem_right);
        return  Math.max(tem_left,tem_right);
    }

LC404左子樹之和

思路:可以中途傳值

    public int sumOfLeftLeaves(TreeNode root) {
        if(root == null ) return 0;
        int sum = 0;

        if(root.left != null && root.left.left ==null && root.left.right == null)
            sum += root.left.val;
        
        return sum + sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
    }    

LC508出現次數最多的子樹元素和

思路:

  • 對每個結點,求其子樹和,放到一個HashMap<子樹和,次數>,

	Map<Integer,Integer> hasMap = new HashMap<>();
    public int[] findFrequentTreeSum(TreeNode root) {

    	dfs_starNode(root  );
    	
    	//此時hashMap中存放各個頂點的子樹和及其出現的次數
    	
    	//遍歷得到出現最多值的次數
    	int maxCount = 0;
    	
    	for(Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
    		if(entry.getValue() > maxCount) 
    			maxCount = entry.getValue();
    	}
    	
    	//根據次數,再次遍歷,得到和
    	List<Integer> resList = new ArrayList<>();
    	for(Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
    		if(entry.getValue() == maxCount)
    			resList.add(entry.getKey());
    	}
    	
    	//list => int[]
    	int res[] = new int[resList.size()];
    	for(int i = 0 ; i < resList.size() ; i++) {
    		res[i] = resList.get(i);
    	}
    	
    	return res;
    	
    }
    
    //從每個點存放 其作爲根時子樹的和
	private void dfs_starNode(TreeNode root) {
		if(root == null)	return  ;
		int val = dfs_getSum(root);
		
//        resList.add(val);
		if(hashMap.containsKey(val))
			hashMap.put(val, hashMap.get(val)+1);
		else
			hashMap.put(val, 1);
		
		dfs_starNode(root.left);
		dfs_starNode(root.right);
		
	}
	
	//求和
	private int dfs_getSum(TreeNode root) {
        // System.out.println("+");
		if(root == null) return 0;

		return root.val +dfs_getSum(root.left) +dfs_getSum(root.right);

	}

LC222.完全二叉樹的結點個數

思路:

  • 在countNodes遞歸函數中,做如下事情:

    • 計算當前結點的左子樹層數left

    • 計算當前結點的右子樹層數right

    • 若left = right :

      ​ 則說明就當前結點而言,左子樹一定是滿的(完全二叉樹性質),返回時加上

    • 若left != right

      右子樹滿,返回時加上遞歸求左子樹的結點數

    public int countNodes(TreeNode root) {
    	if(root == null) return 0;
    	
    	int left= countLevel(root.left);
    	int right = countLevel(root.right);
    	if(left == right)
    		return countNodes(root.right) + ( 1<<left);	//左子樹滿
    	else
    		return countNodes(root.left) + (1 << right); //右子樹滿
    }

	private int countLevel(TreeNode node) {
		int level = 0;
		while(node!= null) {
			level ++;
			node = node.left ; 	//完全二叉樹左子樹具有代表性
		}
			
		return level;
	}

小結

上面的兩類題型(比較判定、路徑累計)從本質上來說都是屬於遍歷性問題的,只不過在遍歷過程中,比基本遍歷的操作更爲麻煩(輔助變量、集合列表) 和/或 邏輯更爲複雜(左右子樹加和、bool運算、回溯、剪枝)

那麼,以前序遍歷的模板爲例,我們可以總結歸納很多相類似問題的解題規律和套路(希望如此):

(1)基礎遍歷模板:

    public void preorder(TreeNode root) {
        if(root == null) return//判空條件不可少,因爲下面要取root的左右子樹,也可能取val

       	/* 這裏每一輪遍歷需要做的事情 */
       	//TODO
        preorder(root.left);	//遞歸對其左子樹做相同的事情
        preorder(root.right);	//遞歸對其右子樹做相同的事情
    }

在每一個結點的關鍵處理步驟中:

/* 這裏每一輪遍歷需要做的事情 */

我們可能用一個變量去累計每個結點的和(已達到我們需要的結果)

也可能會用一個集合去收集,那麼這些個集合作爲參數在遞歸過程中傳遞就好,這一點在前面文章中有詳細介紹:

public void dfs(TreeNode root, ArrayList<Integer>  item, ...  ) {
    //下面操作類似,每一輪可能會有添加,移除等試探回溯的操作

注意到這種返回類型是void,其實我們一般會在外部聲明好一些變量(累計和、結果集合等),在遍歷過程中對其進行改變,因此這種往往作爲一些輔助函數,比如:

    //LC671二叉樹中第二小的節點 
	public int findSecondMinimumValue(TreeNode root) {
        if(root == null) return -1;
        return helper(root, root.val);
    }
	...

	//LC127
	public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new ArrayList<>(); //外部變量,在遞歸中進行修改
        if(root == null) return res;		

        dfs(root, res , new StringBuilder()); //主要進行遞歸的函數
        return res;
    }
	...

(2)帶返回值的遍歷模板:

有時我們會設計遞歸函數返回我們所需要的結果,比如:

  • 返回 數值 類型:

    往往出現在求某些滿足條件的結果,累計(深度、個數等)、路徑(和)

  • 返回 布爾 類型:

    往往出現在對於結點的判斷(比較相等、是否滿足條件)

數值型遍歷模板如下:

    public int dfs(TreeNode root) {
    	if(root == null)	return 0;
    	
    	int left = dfs(root.left); 		//遞歸對root的左子樹求取結果
    	int right = dfs(root.right);	//遞歸對root的右子樹求取結果
    	
        /* 一些對左右子樹的操作、比較、統計等 */
        //TODO
        return 與left和right有關的操作結果
    }

上面那種也可以看做是一股腦走到葉子結點,然後再逐個返回其結果,然後根據返回的結果進行相應的操作。

例如:返回一棵樹中所有結點值的和

private int dfs_getSum(TreeNode root) {
	if(root == null) return 0;
	int left = dfs(root.left); 	
    int right = dfs(root.right);	
	return root.val +left + right;
}

例如:LC104求二叉樹的最大深度:

    public int maxDepth(TreeNode root) {
    	if(root == null)	return 0;
    	
    	int left_height = maxDepth(root.left);
    	int right_height = maxDepth(root.right);
    	return Math.max(left_height, right_height)+1; 	//取左右子樹中深的那個,再加1
    }

判斷:檢查是否平衡(通過返回值的絕對值小於等於1來判斷):

    private int checkBalanced(TreeNode node){
        if(node == null) return 0;

        int left = checkBalanced(node.left); 	//遞歸求左子樹
        if(left == -1) return -1;		
        int right = checkBalanced(node.right);
        if(right == -1) return -1;
        
        //關鍵:判斷每個結點的左右子樹的高度差絕對值是否小於2
        //若是,則加上自身的高度(返回:子樹高度+1)
        //若否:直接返回-1
        return Math.abs(left - right) < 2? Math.max(left , right) + 1: -1;
    }

bool型遍歷模板類似,只是在操作中更多的是比較:

public boolean dfs(TreeNode root){
    if(root == null) return false;
    /*這裏往往有一些滿足結果的結束條件,也有可能有一些剪枝條件*/
    //TODO
    
    boolean left = false , right = false;
    left = dfs(root.left);	//遞歸對左子樹進行判斷
    right = dfs(root.right);	//遞歸對右子樹進行判斷
    
    return  對左右結果left和right進行操作,這裏往往就是bool操作
    
}

例如:LC112路徑總和

判斷棵樹中是否有一條從根到葉子的路徑,使得其結點val的和等於sum。

public boolean hasPathSum(TreeNode root, int sum) {
    if(root == null) return false;
    if(sum == root.val && root.left == null && root.right == null)
        return true;

    boolean left = false, right = false;
    left =  hasPathSum(root.left , sum - root.val) ;
    right = hasPathSum(root.right , sum - root.val);
    
    return left || right;
}

當然,也可以是對兩棵樹之間進行比較

例如:LC100相同的樹

    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p == null && q == null)  return true;
        if(p == null || q == null || p.val != q.val)  return false;

        return isSameTree(p.left , q.left) && isSameTree(p.right, q.right); //遞歸對p樹的左節點和q數的左節點比較,以及p樹的右節點和q樹的右節點比較,結果取交集
    }

以及對稱二叉樹。

(3)一些複雜或是混合的遍歷

這種往往也是由上述兩種構成,拆分就好。比如有時我們會進行下列考察:

  • 對樹每一個結點,都作爲一棵樹去遞歸考察其中結點,如上面[LC572另一個樹的子樹]和[LC437路徑總和3]
  • 有時遞歸中返回的結果值並非是我們想要的,而是在遞歸函數過程中“其它操作”纔是想要的結果。在此類問題中,遞歸函數往往會做兩件事:1、操作a以返回遞歸結果。2、遞歸過程中進行一些另外的操作b,操作b與遞歸結果相結合才能得到該問題結果。。有點繞,參考上面[LC124二叉樹中的最大路徑和]的解法。

本文介紹了二叉樹問題中以遞歸遍歷爲主的習題以及解法思路。重點剖析了相關遍歷模板類型和應用。有關遞歸設計思路以及其他應用可看如下文章:

  1. [算法系列] 搞懂遞歸, 看這篇就夠了 !! 遞歸設計思路 + 經典例題層層遞進
  2. [算法系列] 遞歸應用: 快速排序+歸併排序算法及其核心思想與拓展 … 附贈 堆排序算法
  3. [算法系列] 深入遞歸本質+經典例題解析——如何逐步生成, 以此類推,步步爲營
  4. [算法系列]搞懂DFS(1)——經典例題(數獨遊戲, 部分和, 水窪數目)圖文詳解
  5. [算法系列]搞懂DFS(2)——模式套路+經典例題詳解(n皇后問題,素數環問題)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章