做题目前随便说点
-
树是一种抽象数据类型,一种具有树结构形式的数据集合。
-
节点个数确定,有层次关系。
-
有根节点。
-
除了根,每个节点有且只有一个父节点。
-
没有环路。
-
所有数据结构都可以用链表表示或者用数组表示,树也一样。
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,你除了能确定代码走主分支外,无法细化逻辑的执行过程。