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,你除了能确定代码走主分支外,无法细化逻辑的执行过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值