「这是我参与11月更文挑战的第 12 天,活动详情查看:2021最后一次更文挑战」
刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能
二叉树的最近公共祖先
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)”
示例
示例 1
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3
示例 2
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身
示例 3
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
- 树中节点数目在范围
[2, 10^5]内 10^9 <= Node.val <= 10^9- 所有
Node.val互不相同 p != qp和q均存在于给定的二叉树中
解题
解法一:散列表
思路
对于这种找公共节点的,通常都应该想到利用散列表来实现。对于本题,寻找公共祖先,如果我们可以在一开始就去记录每一个结点的父节点,那么是否就可以找到p、q的每一个祖先结点,如果他们的祖先结点出现相同的,不就是他们的公共祖先,本题要求找到最近的,那第一个相同的,就是它们的公共节点
- 遍历二叉树,通过一个散列表来记录每一个结点的父节点(这难道不会想到后序遍历吗?通过后序遍历,就可以知道左右子树的父节点)
- 然后让p不断的向上移动(因为我们已经通过散列表记录了所有结点的父节点,所以可以轻松实现p的向上移动),记录移动过程中,访问到的祖先结点
- 然后再让q不断的向上移动,在移动过程中,如果发现它的祖先结点已经被访问过(第二步已经记录了访问过的祖先结点),那这个祖先结点就是p、q的最近公共祖先结点
代码
//借助散列表
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode{
parentNodeMap := map[int]*TreeNode{} //记录每个结点的父节点
visisted := map[int]bool{} // 记录p或q已经访问过的祖先结点
//后续遍历实现记录每个结点的父节点
var backTraverse func(*TreeNode)
backTraverse = func(node *TreeNode) {
if node == nil {
return
}
if node.Left != nil {
parentNodeMap[node.Left.Val] = node
backTraverse(node.Left)
}
if node.Right != nil {
parentNodeMap[node.Right.Val] = node
backTraverse(node.Right)
}
}
backTraverse(root)
for p != nil { //p向上遍历并记录访问过的祖先结点
visisted[p.Val] = true
p = parentNodeMap[p.Val]
}
for q != nil {
if visisted[q.Val] {
return q
}
q = parentNodeMap[q.Val]
}
return nil
}
解法二:回溯
思路
看到本题,我们最希望的就是,如果能从下边往上遍历就好了,这样就能找到公共祖先了,这不就是回溯吗?
回溯?二叉树的后续遍历过程在脑子里边走一遍,它是不是回溯的?看图(红色虚线部分)
后续遍历结果:6、7、4、2、5、0、8、1、3
可以看到后续遍历的过程,它是最先处理叶子结点的,也就是从下往上的。剩下的就是我们如何才能找到p、q的公共祖先?
- 如果说有一个结点,在它的左子树中找到了p(或者q),在它的右子树中找到了q(或者p),那这个节点不就是他们的最近公共祖先吗?(因为后序遍历是从下往上的,发现的第一个符合该条件的,肯定是最近的)
- 如果一个结点它的左子树为空,右子树不为空,那右子树的返回值就是我们要找的公共祖先
- 反过来,如果一个结点它的右子树为空,左子树不为空,那左子树的返回值就是我们要找的公共祖先
后两个可能不好理解(假设p、q分别是7和4),看图
清楚了上边的过程,代码就好写了(结合代码再看图会更清晰)
代码
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
// check
if root == nil {
return root
}
// 相等 直接返回root节点即可
if root == p || root == q {
return root
}
// Divide
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
// Conquer
// 左右两边都不为空,则根节点为祖先
if left != nil && right != nil {
return root
}
if left != nil {
return left
}
if right != nil {
return right
}
return nil
}