二叉树中寻找一个元素

先不管最近公共祖先问题,我请你实现一个简单的算法:
给你输入一棵没有重复元素的二叉树根节点root和一个目标值val,请你写一个函数寻找树中值为val的节点。
函数签名如下:

TreeNode find(TreeNode root, int val);

这个函数应该很容易实现对吧,可以用下面的代码进行实现:

// 定义:在以 root 为根的二叉树中寻找值为 val 的节点
TreeNode find(TreeNode root, int val) {// base caseif (root == null) {return null;}// 看看 root.val 是不是要找的if (root.val == val) {return root;}// root 不是目标节点,那就去左子树找TreeNode left = find(root.left, val);if (left != null) {return left;}// 左子树找不着,那就去左子树找TreeNode right = find(root.right, val);if (right != null) {return right;}// 实在找不到了return null;
}

这段代码应该不用我多解释了,但我基于这段代码做一些简单的改写,请你分析一下我的改动会造成什么影响。
首先,修改一下return的位置:

TreeNode find(TreeNode root, int val) {if (root == null) {return null;}// 前序位置if (root.val == val) {return root;}// root 不是目标节点,去左右子树寻找TreeNode left = find(root.left, val);TreeNode right = find(root.right, val);// 看看哪边找到了return left != null ? left : right;
}

这段代码也可以达到目的,但是实际运行的效率会低一些,原因也很简单,如果你能够在左子树找到目标节点,还有没有必要去右子树找了?没有必要。但这段代码还是会去右子树找一圈,所以效率相对差一些。

更进一步,我把对root.val的判断从前序位置移动到后序位置:

TreeNode find(TreeNode root, int val) {if (root == null) {return null;}// 先去左右子树寻找TreeNode left = find(root.left, val);TreeNode right = find(root.right, val);// 后序位置,看看 root 是不是目标节点if (root.val == val) {return root;}// root 不是目标节点,再去看看哪边的子树找到了return left != null ? left : right;
}

这段代码相当于你先去左右子树找,然后才检查root,依然可以到达目的,但是效率会进一步下降。因为这种写法必然会遍历二叉树的每一个节点。

对于之前的解法,你在前序位置就检查root,如果输入的二叉树根节点的值恰好就是目标值val,那么函数直接结束了,其他的节点根本不用搜索。

但如果你在后序位置判断,那么就算根节点就是目标节点,你也要去左右子树遍历完所有节点才能判断出来。

最后,我再改一下题目,现在不让你找值为val的节点,而是寻找值为val1val2的节点,函数签名如下:

TreeNode find(TreeNode root, int val1, int val2);

这和我们第一次实现的find函数基本上是一样的,而且你应该知道可以有多种写法,我选择这样写代码:

// 定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode find(TreeNode root, int val1, int val2) {// base caseif (root == null) {return null;}// 前序位置,看看 root 是不是目标值if (root.val == val1 || root.val == val2) {return root;}// 去左右子树寻找TreeNode left = find(root.left, val1, val2);TreeNode right = find(root.right, val1, val2);// 后序位置,已经知道左右子树是否存在目标值return left != null ? left : right;
}

为什么要写这样一个奇怪的find函数呢?因为最近公共祖先系列问题的解法都是把这个函数作为框架的。

下面一道一道题目来看。

LeetCode题目

力扣第 236 题「二叉树的最近公共祖先」:

给你输入一棵不含重复值的二叉树,以及存在于树中的两个节点pq,请你计算pq的最近公共祖先节点。


两个节点的最近公共祖先其实就是这两个节点向根节点的「延长线」的交汇点,那么对于任意一个节点,它怎么才能知道自己是不是pq的最近公共祖先?

如果一个节点能够在它的左右子树中分别找到pq,则该节点为LCA节点。

这就要用到之前实现的find函数了,只需在后序位置添加一个判断逻辑,即可改造成寻找最近公共祖先的解法代码:

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {return find(root, p.val, q.val);
}// 在二叉树中寻找 val1 和 val2 的最近公共祖先节点
TreeNode find(TreeNode root, int val1, int val2) {if (root == null) {return null;}// 前序位置if (root.val == val1 || root.val == val2) {// 如果遇到目标值,直接返回return root;}TreeNode left = find(root.left, val1, val2);TreeNode right = find(root.right, val1, val2);// 后序位置,已经知道左右子树是否存在目标值if (left != null && right != null) {// 当前节点是 LCA 节点return root;}return left != null ? left : right;
}

find函数的后序位置,如果发现leftright都非空,就说明当前节点是LCA节点,即解决了第一种情况:

find函数的前序位置,如果找到一个值为val1或val2的节点则直接返回,恰好解决了第二种情况:

因为题目说了pq一定存在于二叉树中(这点很重要),所以即便我们遇到q就直接返回,根本没遍历到p,也依然可以断定pq底下,q就是LCA节点。

这样,标准的最近公共祖先问题就解决了,接下来看看这个题目有什么变体。

力扣第 1676 题「二叉树的最近公共祖先 IV」:

依然给你输入一棵不含重复值的二叉树,但这次不是给你输入p和q两个节点了,而是给你输入一个包含若干节点的列表nodes(这些节点都存在于二叉树中),让你算这些节点的最近公共祖先。

函数签名如下:

TreeNode lowestCommonAncestor(TreeNode root, TreeNode[] nodes);

比如还是这棵二叉树:

输入nodes = [7,4,6],那么函数应该返回节点5。

看起来怪吓人的,实则解法逻辑是一样的,把刚才的代码逻辑稍加改造即可解决这道题:

TreeNode lowestCommonAncestor(TreeNode root, TreeNode[] nodes) {// 将列表转化成哈希集合,便于判断元素是否存在HashSet<Integer> values = new HashSet<>();for (TreeNode node : nodes) {values.add(node.val);}return find(root, values);
}// 在二叉树中寻找 values 的最近公共祖先节点
TreeNode find(TreeNode root, HashSet<Integer> values) {if (root == null) {return null;}// 前序位置if (values.contains(root.val)){return root;}TreeNode left = find(root.left, values);TreeNode right = find(root.right, values);// 后序位置,已经知道左右子树是否存在目标值if (left != null && right != null) {// 当前节点是 LCA 节点return root;}return left != null ? left : right;
}

有刚才的铺垫,你类比一下应该不难理解这个解法。

不过需要注意的是,这两道题的题目都明确告诉我们这些节点必定存在于二叉树中,如果没有这个前提条件,就需要修改代码了。

力扣第 1644 题「二叉树的最近公共祖先 II」:

给你输入一棵不含重复值的二叉树的,以及两个节点pq,如果pq不存在于树中,则返回空指针,否则的话返回pq的最近公共祖先节点。

在解决标准的最近公共祖先问题时,我们在find函数的前序位置有这样一段代码:

// 前序位置
if (root.val == val1 || root.val == val2) {// 如果遇到目标值,直接返回return root;
}

我也进行了解释,因为pq都存在于树中,所以这段代码恰好可以解决最近公共祖先的第二种情况:

但对于这道题来说,pq不一定存在于树中,所以你不能遇到一个目标值就直接返回,而应该对二叉树进行完全搜索(遍历每一个节点),如果发现pq不存在于树中,那么是不存在LCA的。

回想我在文章开头分析的几种find函数的写法,哪种写法能够对二叉树进行完全搜索来着?

这种:

TreeNode find(TreeNode root, int val) {if (root == null) {return null;}// 先去左右子树寻找TreeNode left = find(root.left, val);TreeNode right = find(root.right, val);// 后序位置,判断 root 是不是目标节点if (root.val == val) {return root;}// root 不是目标节点,再去看看哪边的子树找到了return left != null ? left : right;
}

那么解决这道题也是类似的,我们只需要把前序位置的判断逻辑放到后序位置即可:

// 用于记录 p 和 q 是否存在于二叉树中
boolean foundP = false, foundQ = false;TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {TreeNode res = find(root, p.val, q.val);if (!foundP || !foundQ) {return null;}// p 和 q 都存在二叉树中,才有公共祖先return res;
}// 在二叉树中寻找 val1 和 val2 的最近公共祖先节点
TreeNode find(TreeNode root, int val1, int val2) {if (root == null) {return null;}TreeNode left = find(root.left, val1, val2);TreeNode right = find(root.right, val1, val2);// 后序位置,判断当前节点是不是 LCA 节点if (left != null && right != null) {return root;}// 后序位置,判断当前节点是不是目标值if (root.val == val1 || root.val == val2) {// 找到了,记录一下if (root.val == val1) foundP = true;if (root.val == val2) foundQ = true;return root;}return left != null ? left : right;
}

这样改造,对二叉树进行完全搜索,同时记录p和q是否同时存在树中,从而满足题目的要求。

接下来,我们再变一变,如果让你在二叉搜索树中寻找pq的最近公共祖先,应该如何做呢?

力扣第 235 题「二叉搜索树的最近公共祖先」:

给你输入一棵不含重复值的二叉搜索树,以及存在于树中的两个节点pq,请你计算pq的最近公共祖先节点。

把之前的解法代码复制过来肯定也可以解决这道题,但没有用到 BST「左小右大」的性质,显然效率不是最高的。

在标准的最近公共祖先问题中,我们要在后序位置通过左右子树的搜索结果来判断当前节点是不是LCA

TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);// 后序位置,判断当前节点是不是 LCA 节点
if (left != null && right != null) {return root;
}

但对于 BST 来说,根本不需要老老实实去遍历子树,由于 BST 左小右大的性质,将当前节点的值与val1val2作对比即可判断当前节点是不是LCA

假设val1 < val2,那么val1 <= root.val <= val2则说明当前节点就是LCA;若root.valval1还小,则需要去值更大的右子树寻找LCA;若root.valval2还大,则需要去值更小的左子树寻找LCA

依据这个思路就可以写出解法代码:

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 保证 val1 较小,val2 较大int val1 = Math.min(p.val, q.val);int val2 = Math.max(p.val, q.val);return find(root, val1, val2);
}// 在 BST 中寻找 val1 和 val2 的最近公共祖先节点
TreeNode find(TreeNode root, int val1, int val2) {if (root == null) {return null;}if (root.val > val2) {// 当前节点太大,去左子树找return find(root.left, val1, val2);}if (root.val < val1) {// 当前节点太小,去右子树找return find(root.right, val1, val2);}// val1 <= root.val <= val2// 则当前节点就是最近公共祖先return root;
}

【LeetCode】最近公共祖先问题相关推荐

  1. LeetCode实战:二叉树的最近公共祖先

    背景 为什么你要加入一个技术团队? 如何加入 LSGO 软件技术团队? 我是如何组织"算法刻意练习活动"的? 为什么要求团队的学生们写技术Blog 题目英文 Given a bin ...

  2. LeetCode实战:二叉搜索树的最近公共祖先

    背景 为什么你要加入一个技术团队? 如何加入 LSGO 软件技术团队? 我是如何组织"算法刻意练习活动"的? 为什么要求团队的学生们写技术Blog 题目英文 Given a bin ...

  3. LeetCode 236. 二叉树的最近公共祖先

    文章目录 解法1:保存祖先节点+逐个判断 解法2:深度优先遍历 解法3:记录祖先节点 https://leetcode-cn.com/problems/lowest-common-ancestor-o ...

  4. leetcode 235. 二叉搜索树的最近公共祖先(Java版,树形dp套路)

    题目 原题地址:leetcode 235. 二叉搜索树的最近公共祖先 说明: 所有节点的值都是唯一的. p.q 为不同节点且均存在于给定的二叉搜索树中. 题解 关于 树形dp 套路,可以参考我的另一篇 ...

  5. 最近公共祖先_[LeetCode] 236. 二叉树的最近公共祖先

    题目链接: https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree 难度:中等 通过率:57.2% 题目描述: ...

  6. LeetCode 2096. 从二叉树一个节点到另一个节点每一步的方向(最小公共祖先)

    文章目录 1. 题目 2. 解题 1. 题目 给你一棵 二叉树 的根节点 root ,这棵二叉树总共有 n 个节点. 每个节点的值为 1 到 n 中的一个整数,且互不相同. 给你一个整数 startV ...

  7. LeetCode 1676. 二叉树的最近公共祖先 IV

    文章目录 1. 题目 2. 解题 1. 题目 给定一棵二叉树的根节点 root 和 TreeNode 类对象的数组(列表) nodes,返回 nodes 中所有节点的最近公共祖先(LCA). 数组(列 ...

  8. LeetCode 1650. 二叉树的最近公共祖先 III(哈希)

    文章目录 1. 题目 2. 解题 1. 题目 给定一棵二叉树中的两个节点 p 和 q,返回它们的最近公共祖先节点(LCA). 每个节点都包含其父节点的引用(指针).Node 的定义如下: class ...

  9. LeetCode 1644. 二叉树的最近公共祖先 II

    文章目录 1. 题目 2. 解题 1. 题目 给定一棵二叉树的根节点 root,返回给定节点 p 和 q 的最近公共祖先(LCA)节点. 如果 p 或 q 之一不存在于该二叉树中,返回 null. 树 ...

  10. LeetCode 863. 二叉树中所有距离为 K 的结点(公共祖先/ DFS+BFS)

    文章目录 1. 题目 2. 解题 2.1 公共祖先 2.2 建图+BFS 1. 题目 给定一个二叉树(具有根结点 root), 一个目标结点 target ,和一个整数值 K . 返回到目标结点 ta ...

最新文章

  1. 星际2虫王iA加入商汤,担任AI研究员,网友:iA vs AI ,是自己训练跟自己打吗?...
  2. Linux基础之shell变量
  3. vue商城项目开发:底部导航样式、顶部导航矩阵和轮播图
  4. 查询计算机端口号被谁占用了
  5. eda可视化_5用于探索性数据分析(EDA)的高级可视化
  6. Ghost for linux 工具备份还原系统
  7. java窗体程序秒表,帮忙解释一个Java小程序(秒表)
  8. net执行oracle的存储过程
  9. 移动APP之专项测试
  10. 简单易学的机器学习算法——极限学习机(ELM)
  11. Win7删除GRUB For DOS启动项
  12. idea使用教程-安装
  13. matlab打反斜杠,[转载]转义字符 反斜杠
  14. SQL语句 —— 查询某天创建的数据(精确到日)
  15. 极限中0除以常数_第七讲 极限存在准则和两个重要极限
  16. k8s - service
  17. 登陆港股市场,阳光保险的 “价值锚点”
  18. ElasticSearch实战(三十六)-Ingest Pipeline 多管道处理器
  19. python 操作键盘,鼠标 。我这个是自动企业微信加好友的,源码可以修改成别的。挺好使!
  20. pip问题:Traceback (most recent call last):File “/home/coin/anaconda3/lib/python3.7/site-packages/pip/_

热门文章

  1. Hblock盘活企业级存储市场
  2. 循环链表构建及解决约瑟夫环、逢七过、链表逆置问题
  3. Excel常用操作(基于实践)
  4. 无人机智能巡检软件设计
  5. 【Codeforces Round #544 (Div. 3) F2. Spanning Tree with One Fixed Degree】DFS
  6. 业务问题:用java将加密的pdf文件转化为图片问题,支持png,jpg,pdf互转
  7. 安卓系统主板链接USB声卡,卡号配置和授权说明
  8. [国产PLC]耐特创新PLC在自动焊锡机中如何运用
  9. ORA-19502ORA-27072
  10. Java使用遗传算法实现智能组卷