求二叉树中两结点的最小公共祖先

直接贴代码 
[cpp]  view plain copy
  1. #include "stdafx.h"  
  2. #include   
  3. #include   
  4. using namespace std;  
  5.   
  6. //定义二叉树的节点  
  7. struct BinaryTreeNode  
  8. {  
  9.    char m_nvalue;  
  10.    BinaryTreeNode * m_pleft;  
  11.    BinaryTreeNode * m_pright;  
  12. };  
  13.   
  14. //创建值为value的节点  
  15. BinaryTreeNode * CreateBinaryTreeNode(char value)  
  16. {  
  17.     BinaryTreeNode * p=new BinaryTreeNode();  
  18.     p->m_nvalue=value;  
  19.     p->m_pleft=NULL;  
  20.     p->m_pright=NULL;  
  21.     return p;   
  22. }  
  23.   
  24. //设置左孩子  
  25. void SetLeftchild(BinaryTreeNode * pRoot,BinaryTreeNode * lchild)  
  26. {  
  27.     if(pRoot==NULL)  
  28.         return;  
  29.     pRoot->m_pleft=lchild;  
  30. }  
  31.   
  32. //设置右孩子  
  33. void SetRightchild(BinaryTreeNode * pRoot,BinaryTreeNode * rchild)  
  34. {  
  35.     if(pRoot==NULL)  
  36.         return;  
  37.     pRoot->m_pright=rchild;  
  38. }  
  39.   
  40. //保存从根到某节点的路径(最重要函数)  
  41. bool getPath(BinaryTreeNode * pRoot,BinaryTreeNode * pNode,list & List)  
  42. {  
  43.     if(pRoot==NULL || pNode ==NULL)  
  44.         return false;  
  45.     if(pRoot==pNode)  
  46.     {  
  47.      List.push_back(pRoot);  
  48.      return true;  
  49.     }  
  50.     List.push_back(pRoot);  
  51.     bool result=getPath(pRoot->m_pleft,pNode,List);  
  52.     if(!result)  
  53.         result=getPath(pRoot->m_pright,pNode,List);  
  54.     if(!result)  
  55.        List.pop_back();  
  56.     return result;  
  57. }  
  58.   
  59. BinaryTreeNode * getCommonParent(BinaryTreeNode * pRoot,BinaryTreeNode * pNode1,BinaryTreeNode * pNode2)  
  60. {  
  61.   list List1;  
  62.   list List2;  
  63.   bool result1=getPath(pRoot,pNode1,List1);  
  64.   bool result2=getPath(pRoot,pNode2,List2);  
  65.   if(result1==false || result2==false)  
  66.     return NULL;  
  67.   BinaryTreeNode * commonParent=NULL;  
  68.   list::iterator ite1=List1.begin();  
  69.   list::iterator ite2=List2.begin();  
  70.   for(;ite1!=List1.end() && ite2!=List2.end();ite1++,ite2++)  
  71.   {  
  72.      if(*ite1==*ite2)  
  73.       commonParent=*ite1;  
  74.      else  
  75.          break;  
  76.   }  
  77.   return commonParent;  
  78. }  
  79.   
  80. void printList(list &List)  
  81. {  
  82.     for(list::iterator ite=List.begin();ite!=List.end();ite++)  
  83.     {  
  84.        cout<<(*ite)->m_nvalue<<" ";  
  85.     }  
  86.     cout<
  87. }  
  88.   
  89. //创建一棵如下所示的二叉树  
  90. //                A  
  91. //              /   \  
  92. //             B     C  
  93. //           /   \  
  94. //          D     E  
  95. //        /  \   /  \  
  96. //       F    G  H   I  
  97. //  
  98.   
  99. int _tmain(int argc, _TCHAR* argv[])  
  100. {  
  101.     
  102.     BinaryTreeNode * pA=CreateBinaryTreeNode('A');  
  103.     BinaryTreeNode * pB=CreateBinaryTreeNode('B');  
  104.     BinaryTreeNode * pC=CreateBinaryTreeNode('C');  
  105.     BinaryTreeNode * pD=CreateBinaryTreeNode('D');  
  106.     BinaryTreeNode * pE=CreateBinaryTreeNode('E');  
  107.     BinaryTreeNode * pF=CreateBinaryTreeNode('F');  
  108.     BinaryTreeNode * pG=CreateBinaryTreeNode('G');  
  109.     BinaryTreeNode * pH=CreateBinaryTreeNode('H');  
  110.     BinaryTreeNode * pI=CreateBinaryTreeNode('I');  
  111.       
  112.     SetLeftchild(pA,pB);  
  113.     SetRightchild(pA,pC);  
  114.     SetLeftchild(pB,pD);  
  115.     SetRightchild(pB,pE);  
  116.     SetLeftchild(pD,pF);  
  117.     SetRightchild(pD,pG);  
  118.     SetLeftchild(pE,pH);  
  119.     SetRightchild(pE,pI);  
  120.       
  121.     BinaryTreeNode * p=getCommonParent(pA,pD,pF);  
  122.     if(p!=NULL)  
  123.         cout<<"Common Parent is "<m_nvalue<
  124.     system("PAUSE");  
  125.     return 0;  

  1. }  







据说这是微软的一道面试题,谁知道呢。

问题描述:找出二叉树上任意两个指定结点的最近共同父结点(LCA,Least Common Ancestor)。

这算不上是一道算法题了,主要还是看数据结构基本知识和编程能力。

有父指针,方法一

首先考虑最简单的情况——二叉树结点数据结构中有父指针。

这是不是非常简单呢?只要分别从两个结点出发向上走到树根,得到两个结点的分支路径,求出这两条路径相互重合部分的最靠下的结点,就是所求的LCA。这只需要O(h)的时空代价(设h是树高,n是树结点数目,平均情况下h = log(n),最坏情况h = n)。

如果想再稍微节省一点儿时间和空间,可以先找出第一条分支路径,并用这些结点建立哈希表,然后从另外一个指定结点开始向上走到树根,每次遇到一个结点就到哈希表中查一下,一旦发现某个结点存在于哈希表中,这个结点就是所求的LCA。这个方法的代码示意如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def FindLCA(node1, node2):
  # Special cases.
  if not node1 or not node2:
    return None
  if node1 is node2:
    return node1

  # Get the first branch path.
  ancestors1 = set()
  while node1:
    ancestors1.add(node1)
    node1 = node1.parent

  # Check if any ancestor of node2 is in the first branch path.
  while node2:
    if node2 in ancestors1:
      return node2    # Got it, the LCA.
    node2 = node2.parent

  # These two nodes have no common ancestor.
  return None

时间和空间复杂度都是O(h)。

有父指针,方法二(2012-04-02 22:10添加)

上面的方法需要至少一个跟深度相当的缓存,在空间上还是有一些浪费的。可以使用更节省空间的方法,就是先计算出两个结点各自的深度,如果深度不同,则将较靠下的一个结点拉上去,直到两个结点在同一深度处。然后同步向根结点前进,首次相遇时则为最小公共祖先。示意代码(python 2.7)如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def FindLCA(node1, node2):
  # Special cases.
  if not node1 or not node2:
    return None
  if node1 is node2:
    return node1

  # Gets each node's depth.
  depth = lambda node: depth(node.parent) + 1 if node else 0
  depth1 = depth(node1)
  depth2 = depth(node2)

  # Pulls up the lower node and makes the two nodes in the same depth.
  mindepth = min(depth1, depth2)
  for i in xrange(depth1 - mindepth): node1 = node1.parent
  for i in xrange(depth2 - mindepth): node2 = node2.parent

  # Finds the common ancestor.
  while node1 and node2:
    if node1 is node2: return node1
    node1 = node1.parent
    node2 = node2.parent

  return None

这样时间复杂度是O(h),空间复杂度是O(1)。

没有父指针,方法一

通常二叉树结点中并没有父结点指针,这时候就要遍历二叉树找到这两个结点,并找出它们的LCA。

在遍历二叉树的时候,很容易就能够记录下根结点到任何结点的分支路径,只要有了分支路径,就可以对比找出LCA。

我们采取前序遍历,即N-L-R的顺序,使用堆栈来避免递归并且记录完整的分支路径。那么,在二叉树中查找指定结点的算法可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Dir:
  (Undef, Left, Right) = range(3)

def FindNodes(root, nodeSet, findAll=True):
  if not root or not nodeSet:
    return None

  pathDict = {}
  path = []
  curr = root
  while curr or path:
    while curr:   # Go down along left branch
      path.append((curr, Dir.Left))
      if curr in nodeSet:
        pathDict[curr] = list(path)
        nodeSet.remove(curr)
        if not nodeSet or not findAll:
          return pathDict
      curr = curr.left
    (curr, dir) = path.pop()
    while dir == Dir.Right:   # Back from right branch
      if not path: return pathDict
      (curr, dir) = path.pop()
    path.append((curr, Dir.Right))  # Trun to right from left
    curr = curr.right

  return pathDict

其中Dir这个类相当于是一个枚举,用来定义当前的分支方向。FindNodes除了需要二叉树根结点外,还需要一个待查找的结点集合。这个函数可以在二叉树中找到所有(或第一个)待查找结点的分支路径,并返回一个字典(结点 --> 路径)。

可以看出,FindNodes函数按照前序顺序遍历整个二叉树,查找指定结点。每遇到一个结点,首先判断它是不是我们要找的,如果不是就沿着左边的分支下降到底,然后转入右侧分支。

有了FindNodes函数的支持,我们就可改写前面的FindLCA函数,即先遍历二叉树求出两个结点的分支路径,然后比较这两条路径找出LCA:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def FindLCA(root, node1, node2):
  # Special cases.
  if not root or not node1 or not node2:
    return None
  if node1 is node2:
    return node1

  # Try to find the two nodes in the tree, and get their branch paths.
  nodeSet = set([node1, node2])
  pathDict = FindNodes(root, nodeSet)
  if nodeSet:
    return None

  path1 = [i[0] for i in pathDict[node1]]
  path2 = [i[0] for i in pathDict[node2]]

  # Compare the two paths, find out the LCA.
  lca = None
  minLen = min(len(path1), len(path2))
  for i in xrange(minLen):
    if path1[i] is not path2[i]:
      break
    lca = path1[i]

  return lca

遍历二叉树查找所有指定的结点需要O(n)时间,O(h)额外空间;对比两条分支路径需要O(h)的时间,因此总的时间代价为O(n),空间代价为O(h)。

没有父结点,方法二(2012-04-05 23:31更新)

上面的代码有点儿太啰嗦了,如果不想缓存整条分支路径,或者只是想让代码更简洁一些,也很容易做到,只需要在遍历查找的时候做点儿小小的改动。关于遍历二叉树可以参考后面的一篇文章:程序基本功之遍历二叉树。这里我将在非递归的前序(N-L-R)遍历基础上修改得到求LCA的程序。

为什么用前序遍历?

首先考察一下LCA的特性,只有两种可能:

  1. LCA就是其中的一个结点,而另一个结点是它的子孙;
  2. 两个结点分别位于LCA的左子树和右子树中。

对于第一种可能,前序遍历时首先找到的结点就是LCA,剩下的事情就是确定第二个结点在它下面。中序和后序也都可以做,但没有这么美妙。

对于第二种可能,假设在前序遍历过程中,首先找到了一个结点(比如下面的H),根据非递归前序遍历的算法特性,这时候栈里一定是依次存储了结点A(根节点)、B、D、G(请自行思考为什么没有C、E、F),再结合LCA的特性,很容易发现,LCA要么是H自身(对应于上面第一种情况),要么就只能是A、B、D或G。剩下的事情就太美妙,继续遍历二叉树,直到找到另外一个结点。这时候看看A、B、D、G和H中还有谁在栈里,最靠下的那个就是LCA。怎么判定谁在栈里?怎么判定最靠下?用辅助变量呗。

    A
   /
  B
 /
C
 \
  D
 /
E
 \
  F
   \
    G
   /
  H

示意程序代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def FindLCA(root, node1, node2):
  nodeset = set([node1, node2])   # Also supports 3 or more nodes.
  s = []          # A stack to help performing N-L-R traversing.
  lca = None      # Records the most possible least common ancestor.
  mindepth = -1   # The depth of lca.
  while root or s:
    if root:
      if root in nodeset:
        nodeset.remove(root)
        if mindepth < 0:
          # Yeah, found the first node. The lca must be itself or already in s.
          lca = root
          mindepth = len(s)
        if not nodeset:
          break
      s.append(root)
      root = root.left
    else:
      root = s.pop()
      if mindepth > len(s):
        lca = root
        mindepth = len(s)
      root = root.right
  return None if nodeset else lca

可以跟程序基本功之遍历二叉树中的非递归前序遍历的程序对比一下,会发现改动之处是非常小的。

这段程序时间复杂度都是O(n),空间复杂度是O(h),这些都是遍历二叉树所需的时间和空间消耗。在遍历之外,就只剩下常数量的时空开销了。


你可能感兴趣的:(求二叉树中两结点的最小公共祖先)