面试题50:求二叉树中两个节点的最近公共祖先

在数据结构中,二叉树是一个比较复杂的数据结构。在面试的时候,很多面试官喜欢通过二叉树来考察一个程序猿的逻辑、代码等基本功。最近正好在看《剑指offer》,并且学习了二叉树的一些相关知识。

在《剑指offer》的第50题,求二叉树中两个节点的最近公共祖先。关于这个题,其实刚看到题目的时候,容易顺着已有的思路忽略题目埋下的坑。首先我们要清楚这棵二叉树是怎样的一棵树,是搜索二叉树?还是只是平常的二叉树?如果是平常的二叉树,它有指向父节点的指针吗?……下来,我们一一分析每一种情况。

1.搜索二叉树

如果当前的树是一棵搜索二叉树,那么题目将会变得简单。搜索二叉树的节点分布是有规律的,如果我们要查找的节点一个在根节点的左边,一个在根节点的右边,那么毫无疑问根节点就是最近的公共祖先。如果不是,我们可以将这种思路递归下去,需要查找最大的节点都小于根节点,那么肯定在左边。右边同理。

这里写图片描述

理清思路,代码实现就简单了。

    Node* FindSameParent(Node* k1, Node* k2)
    {
        K min = 0;
        K max = 0;

        if ((k1->_key) > (k2->_key))
        {
            min = k2->_key;
            max = k1->_key;
        }

        else
        {
            min = k1->_key;
            max = k2->_key;
        }

        Node* cur = _root;
        while (cur)
        {
            if (max >= cur->_key && min <= cur->_key)
                return cur;
            if (max < cur->_key)
                cur = cur->_left;
            if (min>cur->_key)
                cur = cur->_right;
        }
        return NULL;
    }

2.普通的树

如果是一棵普通的树,我们需要考虑其特殊情况,如果它有指向父节点的指针,那么这个题就可以转换成我们之前学习的链表。如果没有父节点,我们就需要对整个树进行遍历去找这个节点。

a.有父节点

如果当前这棵树是有父节点,那么我们就可以顺着根节点一直遍历到我们需要查找的节点, 把这个路径保存到链表中,就变成了寻找两个链表的公共节点。

这里写图片描述

b.无父结点

如果没有父节点,那我们就只能对这个树进行遍历,遍历到一个节点的时候,判断这两个节点其中一个是否存在,如果存在就立即返回。主要是采用递归的思路一次判断。如果一个节点即有第一个需要查找的节点,也有第二个,那么它就是我们所求的节点。因为是递归,所以查找到的节点就是最近的。

这个思路并不难,下来看代码实现。

Node* _FindSameParent(Node* root, Node* k1, Node* k2)
    {
        if (root == NULL)
            return NULL;
        if (root == k1 || root == k2)
            return root;
        Node* _k1 = _FindSameParent(root->_left, k1, k2);
        Node* _k2 = _FindSameParent(root->_right, k1, k2);
        if (_k1 && _k2)
            return root;
        return _k1 ? _k1 : _k2;
    }

以上的思路是我参考《剑指offer》一步一步进行分析的,其实如果这个一个普通的树,我们用上面的思路去分析,再用代码实现是比较复杂的。比如第一个转换成求链表的公共节点,我们都知道链表增删查改的时间复杂度为O(n);第二个用递归,如果这棵树的比较大,堆栈的开销又太大,而且递归的次数较多,时间也比较慢。

在我用链表解决第一种情况时,查看STL的函数时。看到栈的时候,发现这两种情况都可以用栈解决,并且栈的查找时间复杂度为O(1)。这样对整个查找的情况即简单化,而且效率也高。

我们现在用两个栈去保存我们遍历过的路径,然后保证两个栈的大小是一样的,然后进行出栈操作,这样也可以找到最近的公共节点。

这里写图片描述

这里就需要一个函数来得到路径,在查找路径一定要明白:如果在左子树查找成功,那么一定不存在在右子树(这里我们不考虑树中有相同的节点)。如果查找的路径不对,一定要记得最后把该节点pop出,否则栈里的元素就存在问题了。

bool _GetPath(Node* root, Node* k, stack<Node*>& s)
    {
        if (root == NULL)
            return false;
        if (k == root)
        {
            s.push(root);
            return true;
        }
        s.push(root);
        if(_GetPath(root->_left, k, s))
            return true;

        if(_GetPath(root->_right, k, s))
            return true;

        s.pop();
        return false;
    }

查找的代码实现如下。

Node* FindSameParent(Node* k1, Node* k2)
    {
        if (_root == NULL)
            return NULL;

        stack<Node*> s1;
        stack<Node*> s2;

        _GetPath(_root, k1, s1);
        _GetPath(_root, k2, s2);

        while (s1.size() > s2.size())
        {
            s1.pop();
        }

        while (s1.size() < s2.size())
        {
            s2.pop();
        }

        while (s1.top() != s2.top())
        {
            s1.pop();
            s2.pop();
        }
        return s1.top();
    }

写完这道题再来看题目,这道题本身并不难,难得是我们如何考虑到出现的情况,如何把未知的问题转变成我们学过的知识。在写代码前,一定要先分析可能出现的情况,要跳出自己的惯性思维方式,客观去理解看待问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值