高频算法面试题(二十三)- 二叉树的最近公共祖先

224 阅读5分钟

「这是我参与11月更文挑战的第 12 天,活动详情查看:2021最后一次更文挑战

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

二叉树的最近公共祖先

题目来源LeetCode-236. 二叉树的最近公共祖先

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先

中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)”

示例

示例 1

image.png

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 

示例 2

image.png

输入: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 != q
  • p 和 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
}

解法二:回溯

思路

看到本题,我们最希望的就是,如果能从下边往上遍历就好了,这样就能找到公共祖先了,这不就是回溯吗?

回溯?二叉树的后续遍历过程在脑子里边走一遍,它是不是回溯的?看图(红色虚线部分)

image.png

后续遍历结果:6、7、4、2、5、0、8、1、3

可以看到后续遍历的过程,它是最先处理叶子结点的,也就是从下往上的。剩下的就是我们如何才能找到p、q的公共祖先?

  • 如果说有一个结点,在它的左子树中找到了p(或者q),在它的右子树中找到了q(或者p),那这个节点不就是他们的最近公共祖先吗?(因为后序遍历是从下往上的,发现的第一个符合该条件的,肯定是最近的)
  • 如果一个结点它的左子树为空,右子树不为空,那右子树的返回值就是我们要找的公共祖先
  • 反过来,如果一个结点它的右子树为空,左子树不为空,那左子树的返回值就是我们要找的公共祖先

后两个可能不好理解(假设p、q分别是7和4),看图

image.png

清楚了上边的过程,代码就好写了(结合代码再看图会更清晰)

代码

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
}

参考