二叉树的公共祖先

1 引入

今天,我们来看一个二叉树问题中比较经典的问题:给定一个二叉树,找到该树中 p 、 q p、q pq 两个节点的最近公共祖先。

本文首先会介绍什么是最近公共祖先,接着将会针对该问题介绍几种求解方法,也是本文的核心内容。

如果对本文内容有任何疑问以及建议,欢迎评论,一起讨论。

2 最近公共祖先

首先,我们来看一下什么是最近公共祖先。

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

一图胜千言,请看下方示意图。在这个二叉树中,节点 4 和节点 5 的最近公共祖先为节点 3,节点 4 和节点 2 的最近公共祖先为根节点 1。

在明确了问题之后,接下来将给出针对二叉树和二叉搜索树的解决方法。

3 解决方法

3.1 记录父节点

解题思路

既然是要计算两个节点 p 、 q p、q pq 的最近公共祖先,最容易想到的方法是先把二叉树中每个节点的父节点记录下来,然后从要求的两个节点出发,自底向上的找它们两个节点的父节点,遇到的第一个相同的父节点就是这两个节点的最近公共祖先。

具体实现上,我们可以用一个哈希表 p a pa pa 来存储节点与其父节点,键为当前节点的值,值为该节点的父节点。维护另一个哈希表 v i s vis vis,键为某个节点的值(可以唯一表示某一个节点),值为 b o o l bool bool 变量表示该节点是否被访问过。

我们这样做来更新 p a pa pa 哈希表:如果某个节点有左子树(右子树),那么左子树(右子树)的父节点就是该节点,并在左子树(右子树)中递归更新 p a pa pa

在查找 p p p 节点的父节点的时候,我们将迭代到的父节点通过 v i s vis vis 标记为已经访问过;在迭代查找 q q q 节点的父节点时,一旦遇到已经访问过的节点,那么这个被访问的节点就是 p p p q q q 的最近公共祖先了。

示例代码见下方,还是很容易立即的。

示例代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    unordered_map<int, TreeNode*> pa;   // 记录父节点
    unordered_map<int, bool> vis;       // 标记某个节点值是否访问过
    void dfs(TreeNode* root) {
        if (root->left) {
            pa[root->left->val] = root;
            dfs(root->left);
        }
        if (root->right) {
            pa[root->right->val] = root;
            dfs(root->right);
        }
    }
        

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        pa[root->val] = NULL;
        dfs(root);
        while (p != NULL) {
            vis[p->val] = true;
            p = pa[p->val];
        }
        while (q != NULL) {
            if (vis[q->val]) return q;
            q = pa[q->val];
        }

        return NULL;
    }
};

3.2 递归

这个递归的版本参考的是 LeetCode 236. 二叉树最近公共祖先 官方题解 下方评论中 酒橙 的回答。贴上代码如下:

示例代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (q == root || p == root || root == NULL) return root;


        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);

        if (left && right) {
            return root;
        }
        else if (left) {
            return left;
        }
        else {
            return right;
        }

        return NULL;
    }
};

代码分析

递归出口为:只要根节点及以下子节点中找到节点 p 或者 q 中的一个父节点就返回。

如果在 root 的左右子树都返回了一个非空节点,说明左右子树分别找到了 p、q节点的父节点或者 q、p节点的父节点(也就是一边找到了一个节点父节点);如果 root 的左右子树其中有一个返回了空节点,表示返回空节点的那一侧子树没有两个节点的祖先,则答案为刚刚返回的非空节点。

感觉这样的递归已经变味了,已经不是原来大问题的子问题了。

3.3 记录路径

记录路径法以及接下来的一次遍历的方法都是针对二叉搜索树的最近公共祖先问题,上面提到的记录父节点和递归的方法也是同样适用二叉搜索树这个情况的,毕竟二叉搜索树是一个特殊的二叉树。

解题思路

寻找 p 、 q p、q pq 两个节点的公共祖先,我们还可以这样解决:首先找到从根节点分别到达 p 、 q p、q pq 节点的路径 p a t h _ p path\_p path_p p a t h _ q path\_q path_q,然后找出两段路径中最后一个相同的节点,该节点就是从根节点到 p 、 q p、q pq 两个节点的分岔点,也就是这两个节点的最近公共祖先。

该方法能否用在一般的二叉树中呢?答,不能。因为二叉搜索树是一棵特殊的树,该树的左子节点的值小于父节点的值,右子节点的值大于父节点的值,利用该性质可以快速记录从根节点到任一节点的路径。但是普通的二叉树不具备二叉搜索树的节点值之间关系的性质,不容易记录从根节点到任一节点的路径。

具体实现上,利用二叉搜索树的性质来记录从根节点到 p 、 q p、q pq 节点的路径,例如我们需要找到节点 p p p

  • 我们从根节点开始遍历;
  • 如果当前节点就是 p p p,那么成功地找到了节点;
  • 如果当前节点的值大于 p p p 的值,说明 p p p 应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
  • 如果当前节点的值小于 p p p 的值,说明 p p p 应该在当前节点的右子树,因此将当前节点移动到它的右子节点。

最后遍历路径 p a t h _ p path\_p path_p p a t h _ q path\_q path_q,找出两个路径中最后一个相同的节点即为最近公共祖先。

示例代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

class Solution {
public:
    vector<TreeNode*> getPath(TreeNode* root, TreeNode* target) {
        vector<TreeNode*> path;
        TreeNode* node = root;
        while (node != target) {
            path.push_back(node);
            if (target->val < node->val) node = node->left;
            else node = node->right;
        }
        path.push_back(node);   // 加上自身

        return path;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        vector<TreeNode*> path_p = getPath(root, p);
        vector<TreeNode*> path_q = getPath(root, q);

        TreeNode* ancestor;
        for (int i = 0; i < min(path_p.size(), path_q.size()); ++i) {
            if (path_p[i] == path_q[i]) ancestor = path_p[i];
            else break;
        }
        
        return ancestor;
    }
};

3.4 一次遍历

解题思路

整体的遍历过程与两次遍历的解法类似:

  • 我们从根节点开始遍历;
  • 如果当前节点的值大于 p p p q q q 的值,说明 p p p q q q 应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
  • 如果当前节点的值小于 p p p q q q 的值,说明 p p p q q q 应该在当前节点的右子树,因此将当前节点移动到它的右子节点;
  • 如果当前节点的值不满足上述两条要求,说明当前节点就是「分岔点」。此时, p p p q q q 要么在当前节点的不同的子树中(一个在左子树一个在右子树),要么其中一个就是当前节点。

如果对于此方法还不清楚的话,可以参考下方动图。

示例代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        TreeNode* ancestor = root;
        while (1) {
            if (ancestor->val > p->val && ancestor->val > q->val) {
                ancestor = ancestor->left;
            }
            else if (ancestor->val < p->val && ancestor->val < q->val) {
                ancestor = ancestor->right;
            }
            else break;
        }

        return ancestor;
    }
};

总结

以上是针对最近公共祖先问题的四种不同思路的解答,至于选择哪种解决方法要适具体情况而定。对于普通的二叉树问题,容易想到的方法就是记录父节点法,容易想的方法代码量就想对大,不容易想明白的如递归这样的方法,代码量小但是思考量就大。

二叉搜索树中的最近公共祖先问题,利用子节点与父节点值的大小关系可以快速确定从根节点要任一节点的路径,根据两个节点的路径便可以计算出最近公共祖先问题,这便是记录路径的方法。二叉搜索树中的一次遍历法则是从根节点出发将两个节点一并进行处理,无非是三种情况:两个节点都在根节点的左子树中,于是最近公共祖先一定在左子树中,继续迭代;都在右子树中,则最近公共祖先一定在右子树中,继续迭代;左右子树中各有一个节点,此时的父节点就是最近公共祖先,迭代结束,返回结果即可。

解决问题的方法众多,能够完全掌握已知的方法固然好,不能完全掌握也要掌握至少一种。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wang_nn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值