68 - I. 二叉搜索樹的最近公共祖先-深入理解二叉搜索樹前序遍歷

做題目前隨便說點

  • 樹是一種抽象數據類型,一種具有樹結構形式的數據集合。

  • 節點個數確定,有層次關係。

  • 有根節點。

  • 除了根,每個節點有且只有一個父節點。

  • 沒有環路。

  • 所有數據結構都可以用鏈表表示或者用數組表示,樹也一樣。

68 - I. 二叉搜索樹的最近公共祖先

給定一個二叉搜索樹, 找到該樹中兩個指定節點的最近公共祖先。

百度百科中最近公共祖先的定義爲:“對於有根樹 T 的兩個結點 p、q,最近公共祖先表示爲一個結點 x,滿足 x 是 p、q 的祖先且 x 的深度儘可能大(一個節點也可以是它自己的祖先)。”

例如,給定如下二叉搜索樹:  root = [6,2,8,0,4,7,9,null,null,3,5]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-AUxrjDOH-1583717256811)(en-resource://database/1046:1)]

示例 1:

輸入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
輸出: 6
解釋: 節點 2 和節點 8 的最近公共祖先是 6。
示例 2:

輸入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
輸出: 2
解釋: 節點 2 和節點 4 的最近公共祖先是 2, 因爲根據定義最近公共祖先節點可以爲節點本身。

說明:

所有節點的值都是唯一的。
p、q 爲不同節點且均存在於給定的二叉搜索樹中。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof
著作權歸領釦網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。

解題:

審題:

  • 這道題目是典型的二叉搜索樹的題目,就是找節點的題目。
  • 題目給出了需要找的節點的條件:給出兩個節點值,找出這兩個節點的【最近公共祖先】。
  • 我們需要搞清楚公共子節點的定義,然後找出滿足定義和條件的節點即可。
  • 這種題目難就難在定義的理解,如果讓我們找出一個值等於X的節點,這就簡單多了。“值等於X”這個條件和定義一看就懂。
  • 而最近公共祖先的定義比較難理解,如下。
  • 最近公共祖先的定義爲:“對於有根樹 T 的兩個結點 p、q,最近公共祖先表示爲一個結點 x,滿足 x 是 p、q 的祖先且 x 的深度儘可能大(一個節點也可以是它自己的祖先)。”
  • 理解定義的一個方法就是翻譯成多個容易理解的子定義,把一個複雜定義拆分,或者轉譯成一個通俗的定義。
  • 公共祖先的意思就是要求,p、q兩個節點屬於該節點爲根的子樹下。比如像根節點,就是所有節點的公共祖先了,但是題目要求最近公共祖先,所謂的最近公共祖先要求該節點離根節點越遠越好。
  • 然後我就着找規律,如果兩個節點位於該節點的同一個子樹下,就證明還可以再遠離。直到兩個節點一個位於右邊一個位於左邊,即可。
  • 這種題目大多都是找規律,以及看我們對概念的理解。最擔心我們誤解了,這個在所難免,除了反覆確認對定義的理解,確實很難找出問題的答案。
  • 所以我們只能盡力做到不要放過任何題目字眼,對不懂得詞語一定要測定弄懂。其次就是不要放過題目給出得例子(測試用例)。
  • 這道題目還有一種思考方法,就是從二叉搜索樹的定義出發:二叉搜索樹的每個節點有3個屬性。
    • 屬性val,用於判斷指定要求搜索的節點是否爲當前節點。(判定一)
    • 屬性left,用於當被判定的值小於val時,提供進一步搜索指引。或者說,小於val的值都屬於left子樹。(判定二)
    • 屬性right,用於當被判定的值大於val時,提供進一步搜索指引。或者說,大於val的值都屬於right子樹。 (判定三)
  • 其次就是二叉搜索樹常用前序列遍歷來進行搜索。然後對左右子節點剪支。這兩個慣用伎倆。
  • 我們只需要將題目的定義套進二叉搜索樹的定義裏即可。
  • 比如怎麼判斷這個節點是否爲兩個指定節點的最近公共祖先呢?
  • 我們嘗試兩個假設:(暫時不考慮其中一個節點等於當前節點的情況。)
    • 如果不是最近公共祖先會怎樣?會發現,兩個節點要麼都大於val或者都小於val。
    • 如果是最近公共祖先會怎樣?會發現,兩個節點分別位於val的左右兩邊,一個大於val,一個小於val。
  • 基於這個假設我們可以輕易得找出規律了。只要給定兩個節點,一個大於val,一個小於val。就等價於“判定一”。如果都位於左邊,就是“判定二”。如果都位於右邊,就是“判定三”。
  • 既然可以套用二叉搜索樹的前序遍歷搜索算法,那我們只需要拿二叉搜索樹的前序遍歷算法來改造即可。

框架代碼如下:

class Solution {
    public static  int target;
    
    // 三個二叉搜索樹的判定函數
    public static void  judgeTargetNode(TreeNode node) {
    }
    public static boolean isBelongToLeft(TreeNode left) {
    }
    public static boolean  isBelongToRight(TreeNode right) {
    }
    
    void recursive(TreeNode node) {
        // 邊界判斷
        if(...) {
            return;
        }
        // 前需遍歷
        judgeTargetNode(node);
        // 剪支遞歸左節點
        if(isBelongToLeft()) {
            recursive(node.left);
        }
        
        // 剪支遞歸右節點
         if(isBelongToRight()) {
            recursive(node.right);
         }
        
    }
   
    
}
  • 框架寫好了,剩下的事情就是把題目的條件填入上面的3個判定函數。
  • 寫算法題就是抽象出普遍問題的解決方案,抽象出框架,然後找到問題的本事進行擴展。
  • 我們只需要拓展判定函數即可。

開始解題:



/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public static TreeNode target;
    public static int p;
    public static int q;
    
    // 三個二叉搜索樹的判定函數
    public static void judgeTargetNode(TreeNode node) {
        if(node.val > p && node.val < q) {
            Solution.target = node;
        } 
       
        // 一個節點也可以是它自己的祖先
       else if(node.val == p && node.val < q) {
            Solution.target = node;
        }
        
        // 一個節點也可以是它自己的祖先
        else if(node.val > p && node.val == q) {
            Solution.target = node;
        } else {
            Solution.target = new TreeNode(-999999);
        }
        
    }
    public static boolean isBelongToLeft(TreeNode node) {
        if(node.val > p && node.val > q) {
            return true;
        } else {
            return false;
        }
    }
    public static boolean isBelongToRight(TreeNode node) {
        if(node.val < p && node.val < q) {
            return true;
        } else {
            return false;
        }
    }
    
    void recursive(TreeNode node) {
        // 邊界判斷
        if(node == null) {
            return;
        }
        // 前需遍歷
        judgeTargetNode(node);
        // 剪支遞歸左節點
        if(isBelongToLeft(node)) {
            recursive(node.left);
        }
        
        // 剪支遞歸右節點
         if(isBelongToRight(node)) {
            recursive(node.right);
         }
        
    }
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        Solution.p = p.val < q.val ? p.val : q.val ;
        Solution.q = p.val < q.val ? q.val : p.val ;
        Solution.target = null;
        recursive(root);
        return Solution.target;
    }
}




總結:

  • 這道題目還有一個坑 ,就是輸入的節點p,q不一定是有順序。
  • 寫if語句的時候一定要寫else,爲了避免邏輯漏洞。
  • 上一句話,估計大部分都不會放在心上。可是根據非官方數據統計,程序員每寫一個if而不寫else就有75%概率出現BUG,而這些BUG中有50%以上不被人發現,而且發生了BUG後嚴重影響你判斷問題原因。也就是說,你每少寫一個else,就有35%的概率讓自己被bug搞得頭昏腦脹。
  • 最可怕的是即便,你知道少else,會影響你判斷,你也懶得補全,畢竟拉下太多了。補起來太麻煩。最終你不得不選擇走遠路。
  • 也許你會說,多打一些Log不就沒這事了麼?不,這不能成爲你上班寫Bug的理由。就好像不能因爲穿了“尿不溼”就想拉就拉。
  • 也許你會說,多餘的else影響代碼美觀?不,代碼不是妹子,不能當飯喫。要穩定性,而不要美。美化代碼的方法有很多,沒必要爲了美而到處埋雷。
  • 成熟的程序員喜歡給代碼寫else.他能告訴你代碼走到了哪個分支。如果不寫else,你除了能確定代碼走主分支外,無法細化邏輯的執行過程。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章