算法思想一:递归
解题思路:
若 root 是 o1,o2 的 最近公共祖先 ,则只可能为以下情况之一:
- o1 和 o2 在 root 的子树中,且分列 root 的 异侧(即分别在左、右子树中);
- o1 = root ,且 o1 在 root 的左或右子树中;
- o2 = root,且 o2 在 root 的左或右子树中
考虑通过递归对二叉树进行先序遍历,当遇到节点 o1 或 o2 时返回。从底至顶回溯,当节点 o1, o2 在节点 root 的异侧时,节点 root 即为最近公共祖先,则向上返回 root
算法流程:
终止条件:
1、当越过叶节点,则直接返回 null ;
2、当 root 等于 o1,o2 ,则直接返回 root ;
递推工作:
1、开启递归左子节点,返回值记为 left ;
2、开启递归右子节点,返回值记为 right ;
3、返回值: 根据 left 和 right ,可展开为四种情况;
1、当 left 和 right 同时为空 :说明 root 的左 / 右子树中都不包含 o1,o2 ,返回 null ;
2、当 left 和 right 同时不为空 :说明 o1,o2 分列在 root 的 异侧 (分别在 左 / 右子树),因此 root为最近公共祖先,返回 root ;
3、 当 left 为空 ,right 不为空 :o1,o2 都不在 root 的左子树中,直接返回 right 。具体可分为两种情况:
1、 o1,o2 其中一个在 root 的 右子树 中,此时 right 指向 o1(假设为 o1 );
2、 o1,o2 两节点都在 root 的 右子树 中,此时的 right 指向 最近公共祖先节点 ;
4、当 left 不为空 , right 为空 ,返回 root
图解:
代码展示:
Python版本
class Solution: def lowestCommonAncestor(self , root , o1 , o2 ): # write code here return self.dfs(root, o1, o2).val def dfs(self, root, o1, o2): if not root: return root # 特殊情况,如果o1, o2有一个是根节点,则直接返回根节点 if root.val == o1 or root.val == o2: return root # 从根节点的左子树查找最近公共祖先结点 left = self.dfs(root.left, o1, o2) # 从根节点的右子树查找最近公共祖先结点 right = self.dfs(root.right, o1, o2) # 如果左子树递归结果为空,则返回右子树递归结果 if not left: return right # 反之亦然 if not right: return left # o1, o2 分别在左右子树中,公共祖先是根节点 return root
复杂度分析
时间复杂度 : 其中 N 为二叉树节点数;最差情况下,需要递归遍历树的所有节点。
空间复杂度 : 最差情况下,递归深度达到 N ,系统使用 大小的额外空间
算法思想二:哈希表存储父节点
解题思路:
可以用哈希表存储所有节点的父节点,然后我们就可以利用节点的父节点信息从 o1 结点开始不断往上跳,并记录已经访问过的节点,再从 o2 节点开始不断往上跳,如果碰到已经访问过的节点,那么这个节点就是要找的最近公共祖先。
算法流程:
1、从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。
2、从 o1 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。
3、同样,我们再从 o2 节点开始不断往它的祖先移动,如果有祖先已经被访问过,即意味着这是 o1 和 o2 的深度最深的公共祖先
代码展示:
JAVA版本
import java.util.*; /* * public class TreeNode { * int val = 0; * TreeNode left = null; * TreeNode right = null; * } */ public class Solution { /** * * @param root TreeNode类 * @param o1 int整型 * @param o2 int整型 * @return int整型 */ // 结点祖先集合 Map parent = new HashMap(); // 访问结点集合 Set visited = new HashSet(); public void dfs(TreeNode root) { // 循环遍历左子树 添加祖先集合 if (root.left != null) { parent.put(root.left.val, root.val); dfs(root.left); } // 循环遍历右子树 添加祖先集合 if (root.right != null) { parent.put(root.right.val, root.val); dfs(root.right); } } public int lowestCommonAncestor (TreeNode root, int o1, int o2) { // write code here // 深度遍历添加结点及祖先集合 dfs(root); // 遍历存储 o1 的所有祖先 while (parent.get(o1) != null) { visited.add(o1); o1 = parent.get(o1); } // 如果 o2 是 o1 的祖先路径上的某点,直接返回 o2 即为祖先 while (parent.get(o2) != null) { if (visited.contains(o2)) { return o2; } // 添加 o2 的所有祖先 o2 = parent.get(o2); } return root.val; } }
复杂度分析
时间复杂度:,其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,从 o1 和 o2 节点往上跳经过的祖先节点个数不会超过 N,因此总的时间复杂度为 。
空间复杂度:,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 ,哈希表存储每个节点的父节点也需要 的空间复杂度,因此最后总的空间复杂度为 。