LeetCode 236. 二叉树的最近公共祖先(C++)

题目地址:力扣

假设我们的根节点是root,要找的两个节点分别为p和q。从题目给的例子中可以分析最近公共祖先的两种情况:

1、当前节点既不是p也不是q,但是其左子树和右子树分别包含着p和q,那么当前节点就是p和q的最近公共祖先

2、当前节点可能是p或者q的一个,而其左子树或者右子树包含着p或q的另一个,那么当前节点就应该是p和q的最近公共祖先

由于我们判断的是左子树或右子树是否包含p或q,因此在第一种情况并非一定要祖先的左右节点正好是p,q,而是祖先的左子树中包含p,右子树中包含q即可。因此这里就涉及到状态传递,也就是只要左子树中某个节点是p,那么我们就要将这个信息传给其父节点,并一直往上传。由于我们需要先判断左子树和右子树的情况,再往上传,因此很自然就想到了左->右->根的遍历思路,所以我们需要采用后序遍历。而子树是否包含p或q,我们可以用一个布尔值来进行判断。

解法1:后序遍历递归

class Solution {
public:
    bool postOrder(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 首先初始化当前节点的状态为false,代表当前节点既不是p也不是q
        bool curflag = false;
        // 若根节点非空
        if (root != nullptr) {
            // 若根节点是p或者根节点是q的话,将状态置为true
            if (root == p || root == q)
                curflag = 1;
            // 递归遍历左子树和右子树
            bool lflag = postOrder(root->left, p, q);
            bool rflag = postOrder(root->right, p, q);
            // 若左子树和右子树其中有一者是p或q
            if (lflag || rflag ) {
                // 若当前节点是p或q,说明对应第二种情况,即最近祖先就是当前节点
                if (curflag) {
                    resptr = root;
                // 若当前节点不是p也不是q,判断是否左右子树二者分别包含p,q
                } else {
                    // 对应第一种情况,当前节点不是,但左右子树是,最近祖先就是当前节点
                    if (lflag && rflag )
                        resptr = root;
                }
                // 由于左右子树至少一者是,因此将状态传递给当前节点
                curflag = true;
            }
        }
        return curflag;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 后序遍历
        postOrder(root, p, q);
        return resptr;
    }
private:
    // 用于存储最近祖先节点的指针
    TreeNode* resptr;
};

解法2:后序遍历递归(提前终止)

思路:我们换一个角度来看这个问题,由于题目中说p和q一定是两个存在的且在树中的节点。假设我们从root开始找,而且我们只关注与root与它的左右孩子。

  • 假设其左孩子是p,右孩子是空,那么说明q一定在其左孩子的子树里面,因此p一定是q的祖先,p也是二者的最近祖先
  • 假设其左孩子是p,右孩子是q,那么说明当前节点一定是p和q的最近祖先

其核心思想就是,对于某个节点(假设为curNode)来说,假设我找到了一个左孩子或者右孩子是p或者q,那么我就不需要继续往下递归找我的子树了,我只需要往上返回这个节点(假设为findNode)即可,让上层节点(curNode的父节点)判断它的右子树情况,若上层节点的右子树没找到p或者q,说明剩下一个一定在findNode的子树里面。若上层节点的右子树找到了p或者q,说明上层节点就是最近祖先。

因此,我们递归的时候只要找到了p或者q的一者,那么最近祖先要么就是当前找到的这一者,要么就是这一者的父节点,没有例外。具体是这一者还是其父节点,要交给其父节点来判断。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 碰到这三种情况返回,要么空,要么是p,要么是q,否则继续递归
        if (!root || root == p || root == q) 
            return root;
        
        TreeNode *left = lowestCommonAncestor(root->left, p, q);
        TreeNode *right = lowestCommonAncestor(root->right, p, q);
        // 交给父节点来判断,若左右子树分别包含了p和q,那么父节点就是最近祖先
        if (left && right) 
            return root;
        // 若左右子树一者包含p或q,那么就祖先节点就是那一者
        return left ? left : right;
    }
};

解法3:反向构树

思路:既然要求公共最近祖先,而p和q是树中的某一个节点。我只要p和q都往根节点走,那么他们一定会在某个节点相遇或者从某个节点开始走同一条路。但是树的结构又不支持往根节点走,因此我们可以通过遍历树,存下每个节点的父节点,相当于反向构建一棵树。这样我们就可以使用这个方法了。

class Solution {
public:
    // 先序遍历
    void preOrder(TreeNode* root, unordered_map<TreeNode*, TreeNode*> &fa_map)
    {
        if (root != nullptr)
        {
            if (root->left != nullptr)
                fa_map[root->left] = root;
            if (root->right != nullptr)
                fa_map[root->right] = root;
            preOrder(root->left, fa_map);
            preOrder(root->right, fa_map);
        }
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // fa_map用于存储父节点,node_set用于存储找根的时候经历的节点
        unordered_map<TreeNode*, TreeNode*> fa_map;
        unordered_set<TreeNode*> node_set;
        // 定义res节点用于返回
        TreeNode *res = nullptr;
        // 构建反向树
        fa_map[root] = nullptr;
        preOrder(root, fa_map);
        // 先从p找到根,把经历的节点加入node_set
        while (p != nullptr)    
        {
            node_set.insert(p);
            p = fa_map[p];
        }
        // 从q往根找,只要碰到了经历过的节点,那么这个节点就是最近祖先
        while(q != nullptr)
        {
            if (node_set.find(q) != node_set.end()) {
                res = q;
                break;
            }
            q = fa_map[q];
        }
        return res;
    }
};

Accepted

  • 31/31 cases passed (12 ms)
  • Your runtime beats 93.61 % of cpp submissions
  • Your memory usage beats 69.52 % of cpp submissions (13.8 MB)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值