leetcode:面试题68 - II. 二叉树的最近公共祖先

题目来源

面试题68 - II. 二叉树的最近公共祖先

题目描述

在这里插入图片描述

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {

    }
};

题目解析

树形DP

对于二叉树中的任意一个节点root,以及两个节点p、q,p、q的最低公共祖先分为两种情况。

(1)与root无关,即最低公共祖先不是root

  • p、q在左树中某个点汇聚了
  • p、q在右树中某个点汇聚了
  • p、q在左树和右树中不全

(2)与root有关,即最低公共祖先是root

  • 左树中找到p、q中的一个,右树找到另一个
  • root是p,在左树或右树中找到q
  • root是q,在左树或右树中找到p

也就是对于每一个root,它需要分别从它的左右子树中得到如下信息:

  • 找到p了吗?
  • 找到q了吗?
  • 如果两者都找到了,那么其公共祖先是多少

因此定义出一个Info

当得到信息之后,整合出当前节点的Info:

  • 如果curr == p,或者left->findP == true,或者right->findP == true时,则 curr->findP = true
  • 如果curr == q,或者left->findQ == true,或者right->findQ == true时, curr->findQ = true
  • 对于curr->ans:
    • 如果left->ans != nullcurr->ans = left->ans
    • 如果right->ans != nullcurr->ans = right->ans (left->ans与right->ans不可能同时不为空)
    • 否则,如果当前发现 findP && findQ,则curr->ans = curr
class Solution {
    struct Info{
        bool findP;
        bool findQ;
        TreeNode *ans;
        
        Info(bool fP, bool fQ, TreeNode *an){
            findP = fP;
            findQ = fQ;
            ans = an;
        }
    };

    std::shared_ptr<Info> process(TreeNode* root, TreeNode* p, TreeNode* q){
        if(root == nullptr){
            return std::make_shared<Info>(false, false, nullptr);
        }
        
        auto leftInfo = process(root->left, p, q);
        auto rightInfo = process(root->right, p, q);
        bool findP = (root == p) || leftInfo->findP || rightInfo->findP;
        bool findQ = (root == q) || leftInfo->findQ || rightInfo->findQ;
        TreeNode* ans = nullptr;
        if (leftInfo->ans != nullptr) {
            ans = leftInfo->ans;
        } else if (rightInfo->ans != nullptr) {
            ans = rightInfo->ans;
        } else {
            if (findP && findQ) {
                ans = root;
            }
        }
        
        return std::make_shared<Info>(findP, findQ, ans);
    }
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr){
            return root;
        }
        return process(root, p, q)->ans;
    }
};

层次遍历

  • 层次遍历一次:使用一个map将各节点的父节点存储下来:key = 当前节点,value = 当前节点的父节点 。 注意: 如果为根节点,那么它的父节点是null
  • 初始化一个栈s1:将p的路径存储下来,比如p=6时,栈为:6 5 3
  • 初始化第二个栈s2:将q的路径存储下来,比如q=1时,栈为:1 2
  • 初始化一个pre节点,指向null,初始化一个size = Math.min(s1.szie, s2.szie)
  • 循环(size)弹出栈,并更新pre,直到第一个不相同的节点,那么pre就是要找到节点
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr){
            return root;
        }
        
        std::map<TreeNode*, TreeNode*> map;
        map[root] = nullptr;
        std::queue<TreeNode *> queue;
        queue.emplace(root);
        while (!queue.empty()){
            int size = queue.size();
            for (int i = 0; i < size; ++i) {
                TreeNode *top = queue.front(); queue.pop();
                if(top->left != nullptr){
                    queue.emplace(top->left);
                    map[top->left] = top;
                }
                if(top->right != nullptr){
                    queue.emplace(top->right);
                    map[top->right] = top;
                }
            }
        }

        // p、q 为不同节点且均存在于给定的二叉树中
        std::stack<TreeNode *>stack1;
        TreeNode *key = p;
        stack1.push(key);
        while (map[key] != nullptr){
            stack1.push(map[key]);
            key = map[key];
        }

        std::stack<TreeNode *>stack2;
        key = q;
        stack2.push(key);
        while (map[key] != nullptr){
            stack2.push(map[key]);
            key = map[key];
        }

        int size = std::min(stack1.size(), stack2.size());
        TreeNode *pre = nullptr;
        for (int i = 0; i < size; ++i) {
            TreeNode *treeNode1 = stack1.top(); stack1.pop();
            TreeNode *treeNode2 = stack2.top(); stack2.pop();
            if(treeNode1 == treeNode2){
                pre = treeNode2;
            }else{
                break;
            }
        }
        
        return pre;
    }
};

优化:其他我们可以不用遍历全部的树,只需要得到p和q的路径就可以了

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr){
            return root;
        }
        
        std::map<TreeNode*, TreeNode*> parent ;
        parent [root] = nullptr;
        std::queue<TreeNode *> queue;
        queue.emplace(root);
        while (!queue.empty()){
            int size = queue.size();
            for (int i = 0; i < size; ++i) {
                TreeNode *top = queue.front(); queue.pop();
                if(top->left != nullptr){
                    queue.emplace(top->left);
                    parent[top->left] = top;
                }
                if(top->right != nullptr){
                    queue.emplace(top->right);
                    parent [top->right] = top;
                }
            }
        }

        // p、q 为不同节点且均存在于给定的二叉树中
        std::set<TreeNode *>ancestors ;
        while (p != nullptr){
            ancestors.emplace(p);
            p = parent[p];
        }
        
        while (!ancestors.count(q)){
            q = parent[q];
        }
        
        return q;
    }
};

另外写法:

class Solution {
    void fillParentMap(TreeNode* root, std::map<TreeNode *, TreeNode *> &parent){
        if(root->left != nullptr){
            parent[root->left] = root;
            fillParentMap(root->left, parent);
        }
        
        if(root->right != nullptr){
            parent[root->right] = root;
            fillParentMap(root->right, parent);
        }
    }
    
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr){
            return root;
        }

        std::map<TreeNode* , TreeNode* > parentMap;
        parentMap[root] = nullptr;
        fillParentMap(root, parentMap);
        std::set<TreeNode *>p_set;
        TreeNode *cur = p;
        p_set.emplace(cur);
        while (parentMap[cur] != nullptr){
            cur = parentMap[cur];
            p_set.emplace(cur);
        }
        cur = q;
        while (!p_set.count(cur)) {
            cur = parentMap[cur];
        }
        return cur;
    }
};

递归

首先,要想通过递归来实现,就需要先确定临界条件,那么临界条件是什么呢?换句话说,临界条件就是递归中能够直接返回的特殊情况:

  • 第一点则是最常见的“判空”,判断根结点是否是空节点,如果是,那么肯定就可以马上返回了,这是一个临界条件;
  • 再来考虑题意,在以root为根结点的树中找到p结点和q结点的最近公共祖先,那么特殊情况是什么呢?很显然,特殊情况就是根结点就等于q结点或p结点的情况,想一下,如果根结点为二者之一,那么根结点就必定是最近公共祖先了,这时直接返回root即可。由此看来,这道题就一共有三种特殊情况,root == q 、root == p和root==null,这三种情况均直接返回root即可。

根据临界条件,实际上可以发现这道题已经被简化为查找以root为根结点的树上是否有p结点或者q结点,如果有就返回p结点或q结点,否则返回null。
这样一来其实就很简单了,从左右子树分别进行递归,即查找左右子树上是否有p结点或者q结点,就一共有4种情况:

  • 左子树和右子树均找没有p结点或者q结点;(这里特别需要注意,虽然题目上说了p结点和q结点必定都存在,但是递归的时候必须把所有情况都考虑进去,因为题目给的条件是针对于整棵树,而递归会到局部,不一定都满足整体条件)
  • 左子树上能找到,但是右子树上找不到,此时就应当直接返回左子树的查找结果;
  • 右子树上能找到,但是左子树上找不到,此时就应当直接返回右子树的查找结果;
  • 左右子树上均能找到,说明此时的p结点和q结点分居root结点两侧,此时就应当直接返回root结点了。
    综上也不难写出结果了:
class Solution {

public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr || root == p || root == q){
            return root;
        }
        
        TreeNode *left = lowestCommonAncestor(root->left, p, q);
        TreeNode *right = lowestCommonAncestor(root->right, p, q);

        if (left != nullptr && right != nullptr){
            return root;
        }

        if (left == nullptr){
            return right;
        }

        if (right == nullptr){
            return left;
        }

        return nullptr;
    }
};

在这里插入图片描述

也可以这样写:

class Solution {

public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr || root == p || root == q){
            return root;
        }
        
        TreeNode *left = lowestCommonAncestor(root->left, p, q);
        if (left != nullptr && left != q && left != p){
            return left;
        }
        
        TreeNode *right = lowestCommonAncestor(root->right, p, q);

        if (left != nullptr && right != nullptr){
            return root;
        }

        if (left == nullptr){
            return right;
        }

        if (right == nullptr){
            return left;
        }

        return nullptr;
    }
};

进阶:如果查询两个节点的最近公共祖先的操作十分频繁,想让单条查询的查询时间减少

可以先预处理建立一种记录,一行执行每次查询时就可以完全根据记录查询。

设计:用一张哈希表,对二叉树的每个节点记录其父节点(就是上面层次遍历的写法【待整理】)

举个例子,对于如下二叉树

在这里插入图片描述

其哈希表M如下:

在这里插入图片描述
key是二叉树的一个节点,value代表其对应的父节点。只用遍历一次二叉树,这张表就可以建立好,以后每次查询就可以根据这张哈希表进行。

如果想要查询节点4和节点8的最近公共祖先,那么:

  • 把包括节点4在内的所有节点4的祖先放到另一种哈希表A中,A表示节点4到头结点这条路径上所有节点的集合。为{节点4,节点2,节点1}
  • 然后我们使用全局的哈希表,从节点8开始往上逐渐移动到头结点(可能的路径:{节点8,节点7,节点3,节点1})
    • 首先是节点8,发现不再A中,继续上移
    • 然后是节点7,发现不再A中,继续上移
    • 然后是节点3,发现不再A中,继续上移
    • 然后是节点1,在A中,那么节点1就是节点1和节点8的最近公共祖先
class Record{
    std::map<TreeNode *, TreeNode *> map;

    void setMap(TreeNode *head){
        if(head == nullptr){
            return;
        }
        
        if(head->left != nullptr){
            map.insert({head->left, head});
        }

        if(head->right != nullptr){
            map.insert({head->right, head});
        }
        
        setMap(head->left);
        setMap(head->right);
    }
public:
    explicit  Record(TreeNode *head){
        if(head != nullptr){
            map.insert({head, nullptr});
        }
    }
    
    TreeNode *query(TreeNode *o1, TreeNode *o2){
        std::set<TreeNode *>path;
        while (map.count(o1)){
            path.insert(o1);
            o1 = map[o1];
        }
        
        while (!path.count(o2)){
            o2 = map[o2];
        }
        return o2;
    }
};

进阶:给定二叉树的头结点head,同时给定所有想要进行的查询。二叉树的节点数为N,查询条数为M,请在时间复杂度为O(N + M)内返回所有查询的结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值