深入二叉树两个结点的最低共同父结点的详解

 更新时间:2013年05月23日 18:21:23   作者:  
本篇文章是对二叉树两个结点的最低共同父结点进行了详细的分析介绍,需要的朋友参考下
题目:二叉树的结点定义如下:
复制代码 代码如下:

struct TreeNode
   {
              int m_nvalue;
             TreeNode* m_pLeft;
             TreeNode* m_pRight;
};

输入二叉树中的两个结点,输出这两个结点在数中最低的共同父结点。
分析:求数中两个结点的最低共同结点是面试中经常出现的一个问题。这个问题至少有两个变种。
第一变种是二叉树是一种特殊的二叉树:查找二叉树。也就是树是排序过的,位于左子树上的结点都比父结点小,而位于右子树的结点都比父结点大。我们只需要从根结点开始和两个结点进行比较。如果当前结点的值比两个结点都大,则最低的共同父结点一定在当前结点的左子树中。如果当前结点的值比两个结点都小,则最低的共同父结点一定在当前结点的右子树中。
第二个变种是树不一定是二叉树,每个结点都有一个指针指向它的父结点。于是我们可以从任何一个结点出发,得到一个到达树根结点的单向链表。因此这个问题转换为求两个单向链表的第一个公共结点。
现在我们回到这个问题本身。所谓共同的父结点,就是两个结点都出现在这个结点的子树中。因此我们可以定义一函数,来判断一个结点的子树中是不是包含了另外一个结点。这不是件很难的事,我们可以用递归的方法来实现:
复制代码 代码如下:

/*
// If the tree with head pHead has a node pNode, return true.
// Otherwise return false.
*/
bool HasNode(TreeNode* pHead, TreeNode* pNode)
{
 if(pHead == pNode)
  return true;
 bool has = false;
 if(pHead->m_pLeft != NULL)
  has = HasNode(pHead->m_pLeft, pNode);
 if(!has && pHead->m_pRight != NULL)
  has = HasNode(pHead->m_pRight, pNode);
 return has;
}

我们可以从根结点开始,判断以当前结点为根的树中左右子树是不是包含我们要找的两个结点。如果两个结点都出现在它的左子树中,那最低的共同父结点也出现在它的左子树中。如果两个结点都出现在它的右子树中,那最低的共同父结点也出现在它的右子树中。如果两个结点一个出现在左子树中,一个出现在右子树中,那当前的结点就是最低的共同父结点。基于这个思路,我们可以写出如下代码:
复制代码 代码如下:

/*
// Find the last parent of pNode1 and pNode2 in a tree with head pHead
*/
TreeNode* LastCommonParent_1(TreeNode* pHead, TreeNode* pNode1, TreeNode* pNode2)
{
 if(pHead == NULL || pNode1 == NULL || pNode2 == NULL)
  return NULL;
 // check whether left child has pNode1 and pNode2
 bool leftHasNode1 = false;
 bool leftHasNode2 = false;
 if(pHead->m_pLeft != NULL)
 {
  leftHasNode1 = HasNode(pHead->m_pLeft, pNode1);
  leftHasNode2 = HasNode(pHead->m_pLeft, pNode2);
 }
 if(leftHasNode1 && leftHasNode2)
 {
  if(pHead->m_pLeft == pNode1 || pHead->m_pLeft == pNode2)
   return pHead;
  return LastCommonParent_1(pHead->m_pLeft, pNode1, pNode2);
 }
 // check whether right child has pNode1 and pNode2
 bool rightHasNode1 = false;
 bool rightHasNode2 = false;
 if(pHead->m_pRight != NULL)
 {
  if(!leftHasNode1)
   rightHasNode1 = HasNode(pHead->m_pRight, pNode1);
  if(!leftHasNode2)
   rightHasNode2 = HasNode(pHead->m_pRight, pNode2);
 }
 if(rightHasNode1 && rightHasNode2)
 {
  if(pHead->m_pRight == pNode1 || pHead->m_pRight == pNode2)
   return pHead;
  return LastCommonParent_1(pHead->m_pRight, pNode1, pNode2);
 }
 if((leftHasNode1 && rightHasNode2) || (leftHasNode2 && rightHasNode1))
  return pHead;
 return NULL;
}

接着我们来分析一下这个方法的效率。函数HasNode的本质就是遍历一棵树,其时间复杂度是O(n)(n是树中结点的数目)。由于我们根结点开始,要对每个结点调用函数HasNode。因此总的时间复杂度是O(n^2)。
我们仔细分析上述代码,不难发现我们判断以一个结点为根的树是否含有某个结点时,需要遍历树的每个结点。接下来我们判断左子结点或者右结点为根的树中是否含有要找结点,仍然需要遍历。第二次遍历的操作其实在前面的第一次遍历都做过了。由于存在重复的遍历,本方法在时间效率上肯定不是最好的。
前面我们提过如果结点中有一个指向父结点的指针,我们可以把问题转化为求两个链表的共同结点。现在我们可以想办法得到这个链表。我们在这里稍作变化即可:
复制代码 代码如下:

/*
// Get the path form pHead and pNode in a tree with head pHead
*/
bool GetNodePath(TreeNode* pHead, TreeNode* pNode, std::list<TreeNode*>& path)
{
 if(pHead == pNode)
  return true;
 path.push_back(pHead);
 bool found = false;
 if(pHead->m_pLeft != NULL)
  found = GetNodePath(pHead->m_pLeft, pNode, path);
 if(!found && pHead->m_pRight)
  found = GetNodePath(pHead->m_pRight, pNode, path);
 if(!found)
  path.pop_back();
 return found;
}

 由于这个路径是从跟结点开始的。最低的共同父结点就是路径中的最后一个共同结点:
复制代码 代码如下:

/*
// Get the last common Node in two lists: path1 and path2
*/
TreeNode* LastCommonNode
(
 const std::list<TreeNode*>& path1,
 const std::list<TreeNode*>& path2
 )
{
 std::list<TreeNode*>::const_iterator iterator1 = path1.begin();
 std::list<TreeNode*>::const_iterator iterator2 = path2.begin();  
 TreeNode* pLast = NULL;
 while(iterator1 != path1.end() && iterator2 != path2.end())
 {
  if(*iterator1 == *iterator2)
   pLast = *iterator1;
  iterator1++;
  iterator2++;
 }
 return pLast;
}

有了前面两个子函数之后,求两个结点的最低共同父结点就很容易了。我们先求出从根结点出发到两个结点的两条路径,再求出两条路径的最后一个共同结点。代码如下:
复制代码 代码如下:

/*
// Find the last parent of pNode1 and pNode2 in a tree with head pHead
*/
TreeNode* LastCommonParent_2(TreeNode* pHead, TreeNode* pNode1, TreeNode* pNode2)
{
 if(pHead == NULL || pNode1 == NULL || pNode2 == NULL)
  return NULL;
 std::list<TreeNode*> path1;
 GetNodePath(pHead, pNode1, path1);
 std::list<TreeNode*> path2;
 GetNodePath(pHead, pNode2, path2);
 return LastCommonNode(path1, path2);
}

这种思路的时间复杂度是O(n),时间效率要比第一种方法好很多。但同时我们也要注意到,这种思路需要两个链表来保存路径,空间效率比不上第一个方法。

相关文章

  • 教你使用Matlab制作图形验证码生成器(app designer)

    教你使用Matlab制作图形验证码生成器(app designer)

    这篇文章主要和大家分享如何利用Matlab制作一款图形验证码生成器,文中的实现步骤讲解详细,感兴趣的小伙伴可以跟随小编动手试一试
    2022-02-02
  • 使用C语言编写圣诞表白程序

    使用C语言编写圣诞表白程序

    圣诞节快到了,让我们用C语言制作一个圣诞表白程序吧,下面通过本文学习下实现代码
    2016-12-12
  • c++中移动语义和完美转发及易错点

    c++中移动语义和完美转发及易错点

    C++ 中的移动语义和完美转发是 C++11 引入的两个重要特性,它们分别用于提高性能和灵活性,这篇文章主要介绍了c++中移动语义和完美转发,需要的朋友可以参考下
    2023-09-09
  • C++初级线程管理

    C++初级线程管理

    这篇文章主要介绍了C++初级线程管理,C++11中提供了std::thread库,本文将从线程的启动、线程等待、线程分离、线程传参、线程识别等几个方面介绍初级线程管理的知识,需要的朋友可以参考一下
    2021-12-12
  • 详解C语言未初始化的局部变量是多少

    详解C语言未初始化的局部变量是多少

    这篇文章主要给大家介绍了关于C语言未初始化的局部变量是多少,文中通过示例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-07-07
  • C语言深入探究斐波那契数列

    C语言深入探究斐波那契数列

    斐波那契数一般指斐波那契数列。 斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为兔子数列
    2022-05-05
  • C++ 网络连通性检测的实现方法

    C++ 网络连通性检测的实现方法

    这篇文章主要介绍了C++ 网络连通性检测的实现方法的相关资料,这里提供实例帮助大家实现这样的功能,需要的朋友可以参考下
    2017-09-09
  • MFC自定义消息的实现方法

    MFC自定义消息的实现方法

    这篇文章主要介绍了MFC自定义消息的实现方法,通过该示例可以更好的理解MFC的消息封装机制,以便更加灵活的打造个性化的windows应用程序,需要的朋友可以参考下
    2014-07-07
  • C语言由浅入深理解指针

    C语言由浅入深理解指针

    C语言这门课程在计算机的基础教学中一直占有比较重要的地位,然而要想突破C语言的学习,对指针的掌握是非常重要的,本文将具体针对指针的基础做详尽的介绍
    2022-05-05
  • C++多态与虚拟之C++编译器对函数名的改编(Name Mangling)

    C++多态与虚拟之C++编译器对函数名的改编(Name Mangling)

    在Windows DLLs中,使用C++编写的DllMain()等callback函数需避免C++编译器进行name mangling,因此需使用extern "C",这篇文章主要介绍了C++多态与虚拟:C++编译器对函数名的改编(Name Mangling),需要的朋友可以参考下
    2024-04-04

最新评论