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

所属分类: 软件编程 / C 语言 阅读数: 122
收藏 0 赞 0 分享
题目:二叉树的结点定义如下:
复制代码 代码如下:

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),时间效率要比第一种方法好很多。但同时我们也要注意到,这种思路需要两个链表来保存路径,空间效率比不上第一个方法。
更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多