做題目前隨便說點
-
樹是一種抽象數據類型,一種具有樹結構形式的數據集合。
-
節點個數確定,有層次關係。
-
有根節點。
-
除了根,每個節點有且只有一個父節點。
-
沒有環路。
-
所有數據結構都可以用鏈表表示或者用數組表示,樹也一樣。
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,你除了能確定代碼走主分支外,無法細化邏輯的執行過程。