在二叉树中找到共同的祖先

8
这个问题是在面试中问我的:我有一棵二叉树,我必须找到两个随机节点的公共祖先(父节点)。我还有一个指向根节点的指针。
我的答案是:分别遍历这两个节点的树,直到到达期望的节点。在遍历时,同时将元素和下一个地址存储在链表中。然后我们就有了两个链表。因此,尝试比较这两个链表,两个链表中最后一个相同的节点就是父节点。
我认为这个解决方案是正确的,如果我错了,请纠正我。如果这个解决方案是正确的,那么请问是否有比这更好的解决方案?

6
@关闭投票者:请添加一条评论,说明您为什么要投票关闭? - Vijay
你有指向父级的指针吗? - Kiril Kirov
我有一个指向根节点的指针。 - Vijay
在每个节点?那有点奇怪。 - Kiril Kirov
我需要向我的函数传递三个参数。根节点、node1和node2。我不知道这有什么奇怪的? - Vijay
显示剩余2条评论
10个回答

7
也许看起来有些傻,但是这种方法很实用:从每个节点生成路径,并将其存储为“L”和“R”的字符串。翻转这些字符串,取最长的公共前缀 - 现在您就有了到公共祖先的路径。

6
将指针分别设置在两个随机节点上。 通过遍历到顶部并计算从根节点开始的距离,找到每个节点的深度。 然后再次将指针设置在两个节点上。对于较深的节点,向上遍历直到两个指针在同一个深度上。 然后对两个节点向上遍历,直到指针指向相同的节点。那就是祖先节点。 “向上遍历”只是指将指针移动到当前节点的父节点。
编辑以澄清:关键的想法是当两个节点处于相同的深度时,你可以通过简单的遍历快速找到共同的父节点。因此,您可以将较低的节点上升直到两者位于相同的深度,然后向上遍历。抱歉我不太了解C,否则我会写代码,但这个算法应该能回答你的问题。
再次编辑:我的方法在平衡树中运行时间为O(log(n)),内存为O(1)。
另一种编辑:在不平衡的树中,最坏情况的性能为O(n)。感谢@DaveCahill

树的遍历不计入时间复杂度计算吗?我原本认为计算深度需要O(n)的时间,其中n是节点深度,使得算法的时间复杂度为O(n)或线性时间。我似乎找不到一个好的资源来讨论这一点,所以如果你有一个的话,我会很感兴趣。 - Dave Cahill
@DaveCahill 你说得完全正确,但是我将n视为元素的数量,而不是树的深度。按照这个考虑,时间复杂度只有O(log(n))。 - AlexQueue
1
对于平衡二叉树来说,这很有道理。但我猜对于非平衡树来说,时间复杂度会是O(n),因为大O符号表示最坏情况。在非平衡二叉树的最坏情况下(根节点的左子节点一直只有左孩子,右子节点一直只有右孩子),该算法将访问所有n个元素两次。 - Dave Cahill

3

我认为你可以同时搜索两个节点,当搜索分叉的时候,这个分叉点就是它们的共同祖先。

commonAncestor tree a b:
  value := <value of node 'tree'>
  if (a < value) && (b < value)
  then commonAncestor (left tree) a b
  else if (a > value) && (b > value)
  then commonAncestor (right tree) a b
  else tree

有趣的是,这种方法可以扩展到超过两个节点(检查它们是否都在tree的左侧等)。

2
进行层序遍历,对于我们遇到的每个节点,检查其子节点。如果它们是提供的随机节点,则找到祖先节点。

编辑1:

以下是大纲

struct _node {
   my_type data;
   struct _node *left;
   struct _node *right;
}

q = queue_create ();
queue_insert (q, head);
temp = head;
while (!empty (q))
{
    temp = queue_remove (q);
 if (
      (temp->left == my_random_node_1) && (head->right == my_random_node_2) ||
      (temp->left == my_random_node_2) && (head->right == my_random_node_1)
    )
    {
       /* temp is the common parent of the two target notes */
       /* Do stuffs you need to do */
    }

    /* Enqueue the childs, so that in successive iterations we can
     * check them, by taking out from the queue
     */
    push (q, temp->left);
    push (q, temp->right);
}

更新

之前的算法只能找到共同的父节点(直接祖先),因此如果随机选择的两个节点不是公共父节点的子节点,则无法找到答案。

下面的算法将找到共同的祖先,而不仅仅是父节点。

我认为以下算法可以解决问题:

进行二叉树的后序遍历,并查找随机节点1 r1,如果找到则将其标记为状态一,并继续查找第二个节点,如果找到则更新状态变量为状态二,停止搜索并返回。状态变量应由每个节点传递给其父节点(递归)。第一个遇到状态二的节点就是公共祖先。

算法的实现如下:

int postorder (node *p, int r1, int r2)
{
  int x = 0; /* The state variable */
  if (p->data == TERMINAL_VAL)
    return x;

  /* 0x01 | 0x02 = 0x03 threfore 
   * state one is when x = 0x01 or x = 0x02
   * state two is when x = 0x03
   */
  if (p->data == r1)
    x |= 0x01;
  else if (p->data == r2)
    x |= 0x02;

  /* if we have x in state two, no need to search more
   */
  if (x != 0x03)
    x |= postorder (p->left, r1, r2);
  if (x != 0x03)
    x |= postorder (p->right, r1, r2);

  /* In this node we are in state two, print node if this node
   * is not any of the two nodes r1 and r2. This makes sure that
   * is one random node is an ancestor of another random node
   * then it will not be printed instead its parent will be printed
   */
  if ((x == 0x03) && (p->data != r1) && (p->data != r2))
  {
   printf ("[%c] ", p->data);
   /* set state variable to 0 if we do not want to print 
    * the ancestors of the first ancestor 
    */
   x = 0;
  }

  /* return state variable to parent
   */    
  return x;
}

我认为这个算法能够正确运行,尽管我还没有证明它的正确性。有一个缺点,即如果一个节点是另一个节点的子节点,则它只会打印另一个节点的父节点,而不是打印它们两个的共同父节点。如果一个随机节点是另一个随机节点的祖先,则它将打印其父节点,而不是打印祖先随机节点。如果其中一个随机节点是根节点,它将不会打印任何内容,因为它始终是另一个随机节点的祖先,因此它们的共同祖先不存在。在这种特殊情况下,函数将在 main 中返回0x03,可以检测到。
由于该算法执行后序遍历,因此需要O(n)的执行时间和O(n)的内存。此外,由于搜索在找到两个节点后立即停止,因此节点越浅,搜索结束得越快。 更新 以下是一些更多的讨论:如何在任何二叉树中查找两个节点的最低公共祖先?

请您能否详细说明一下这个问题! - Vijay
@phoxis:我觉得你误解了问题。你的解决方案似乎是回答了一个稍微不同的问题。 - Adrian McCarthy
@Adrian:我认为你是对的。它需要搜索祖先而不仅仅是父级。在这种情况下,我认为需要两个堆栈。让我来处理一下,看看我能否改进答案。 - phoxis

1
这个问题已经被广泛研究过,有已知的算法可以在线性时间内解决。这篇论文描述了许多不同的方法来解决它。诚然,这是一篇研究论文,因此算法有点棘手,但它所描述的一些方法实际上是相当可行的。

0

以上所述的方法行不通,因为您假设这两个节点都是某个特定节点的直接子节点...

            8
     10           12
 7             

我给出的节点是7和12,答案必须是8。我们这样做吧。

    find(root, d1, d2, n1=null, n2=null)
     {
          if(n1 && n2) return;
          if(!root) return;

          else  if(root -> d == d1 ) n1 = root;
          else  if(root -> d == d2 ) n2 = root;                     
          find(root->left, d1, d2, n1, n2);
          find(root->right, d1, d2, n1, n2);
     }

     LCA(root, d1, d2)
     {
         node *n1=null, *n2=null;
         find(root, d1, d2, n1, n2);
         if(n1 == null || n2 == null )error 'nodes not present' exit(0);
         findIntersect(n1, n2); 
     }
     findInterSect(node *n1, node *n2)
     {
        l1 = length(n1);
        l2 = length(n2);
        node *g = n2, *l = n1;
        diff = abs(l1 - l2);
        if(l1>l2) g = n1 l =n2 
        while(diff) g = g->parent; diff--;
        // now both nodes are at same level
        while(g != l) g= g->parent, l = l->parent;
     }

0

伪代码:

node *FindCommonAncestor(node *root, node *node1, node *node2) {
  node *current = node1;
  node_list temp_list;
  temp_list.add(current);
  while (current != root) {
    current = current.parent;
    temp_list.add(current);
  }
  current = node2;
  while (current not in temp_list) {
    current = current.parent;
  }
  return current;
}

如果节点肯定是同一棵树的一部分,那么它们肯定有一个共同的祖先(即使在最坏的情况下也是根节点)。因此,它将始终终止,并且没有错误条件需要担心。
第一个循环运行n次,其中n是node1的深度,因此它是O(n)。第二个循环运行m次,其中m是node2的深度。对temp列表的查找是(最坏情况下)n。因此,第二个循环是O(m*n),并且它占主导地位,因此函数在O(m*n)中运行。
如果您使用良好的集合数据结构(例如哈希表)作为临时空间而不是列表,则可以将查找削减到(通常为)O(1),而不增加将节点添加到temp的成本。这将将我们的函数时间降至O(m)。
无论哪种方式,空间要求都是O(n)。

由于我们事先不知道n和m的值,因此让我们用树中节点的总数S来表示。如果树是平衡的,则n和m都受到log_2(S)的限制,因此运行时间为O(log_2(S)^2)。Log_2非常强大,因此S必须变得非常大,我才会担心2的幂次方。如果树不平衡,则失去了log_2(树可能实际上退化为链表)。因此,最坏情况(当一个节点是完全退化树的根,另一个节点是叶子)的时间复杂度为O(S^2)。


0

你好,这将返回树的根和传递给节点的val1、val2数据值的最低祖先节点值。

int CommonAncestor(node *root, int val1,int val2) 
{

    if(root == NULL || (! root->left && ! root->right  )
        return false;

        while(root)
        {
            if(root->data < val1 && root->data < val2)
             {
                root = root->left;
             }
             else if(root->data > val1 && root->data > val2)
            {
                root= root->right;
            }
            else
              return root->data;      
        }
}

0

以下是两种在c# (.net)中的方法(上面都有讨论)供参考:

  1. 在二叉树中找到LCA的递归版本(O(N) - 最多每个节点都会被访问) 解决方案的主要点是LCA是(a) 二叉树中仅有的一个节点,其中两个元素分别位于子树(左侧和右侧)的一侧。 (b) 并且无论哪个节点位于哪一侧都没有关系 - 最初我尝试保留这些信息,但显然递归函数变得非常混乱。一旦我意识到这一点,它就变得非常优雅。

  2. 搜索两个节点(O(N)),并跟踪路径(使用额外空间 - 因此,如果二叉树平衡良好,则#1可能更优,即使空间可能可以忽略不计,因为额外的内存消耗将只是O(log(N)))。 因此,路径被比较(基本上类似于接受的答案 - 但路径是通过假定指针节点不存在于二叉树节点中来计算的)

  3. 仅为完整性而提供(与问题无关),BST中的LCA(O(log(N))

  4. 测试

递归:

private BinaryTreeNode LeastCommonAncestorUsingRecursion(BinaryTreeNode treeNode, 
            int e1, int e2)
        {
            Debug.Assert(e1 != e2);
            
            if(treeNode == null)
            {
                return null;
            }
            if((treeNode.Element == e1)
                || (treeNode.Element == e2))
            {
                //we don't care which element is present (e1 or e2), we just need to check 
                //if one of them is there
                return treeNode;
            }
            var nLeft = this.LeastCommonAncestorUsingRecursion(treeNode.Left, e1, e2);
            var nRight = this.LeastCommonAncestorUsingRecursion(treeNode.Right, e1, e2);
            if(nLeft != null && nRight != null)
            {
                //note that this condition will be true only at least common ancestor
                return treeNode;
            }
            else if(nLeft != null)
            {
                return nLeft;
            }
            else if(nRight != null)
            {
                return nRight;
            }
            return null;
        }

上述私有递归版本是由以下公共方法调用的:

public BinaryTreeNode LeastCommonAncestorUsingRecursion(int e1, int e2)
        {
            var n = this.FindNode(this._root, e1);
            if(null == n)
            {
                throw new Exception("Element not found: " + e1);
            }
            if (e1 == e2)
            {   
                return n;
            }
            n = this.FindNode(this._root, e2);
            if (null == n)
            {
                throw new Exception("Element not found: " + e2);
            }
            var node = this.LeastCommonAncestorUsingRecursion(this._root, e1, e2);
            if (null == node)
            {
                throw new Exception(string.Format("Least common ancenstor not found for the given elements: {0},{1}", e1, e2));
            }
            return node;
        }

通过跟踪两个节点的路径来解决:

public BinaryTreeNode LeastCommonAncestorUsingPaths(int e1, int e2)
        {
            var path1 = new List<BinaryTreeNode>();
            var node1 = this.FindNodeAndPath(this._root, e1, path1);
            if(node1 == null)
            {
                throw new Exception(string.Format("Element {0} is not found", e1));
            }
            if(e1 == e2)
            {
                return node1;
            }
            List<BinaryTreeNode> path2 = new List<BinaryTreeNode>();
            var node2 = this.FindNodeAndPath(this._root, e2, path2);
            if (node1 == null)
            {
                throw new Exception(string.Format("Element {0} is not found", e2));
            }
            BinaryTreeNode lca = null;
            Debug.Assert(path1[0] == this._root);
            Debug.Assert(path2[0] == this._root);
            int i = 0;
            while((i < path1.Count)
                && (i < path2.Count)
                && (path2[i] == path1[i]))
            {
                lca = path1[i];
                i++;
            }
            Debug.Assert(null != lca);
            return lca;
        }

FindNodeAndPath的定义位置

private BinaryTreeNode FindNodeAndPath(BinaryTreeNode node, int e, List<BinaryTreeNode> path)
        {
            if(node == null)
            {
                return null;
            }
            if(node.Element == e)
            {
                path.Add(node);
                return node;
            }
            var n = this.FindNodeAndPath(node.Left, e, path);
            if(n == null)
            {
                n = this.FindNodeAndPath(node.Right, e, path);
            }
            if(n != null)
            {
                path.Insert(0, node);
                return n;
            }
            return null;
        }

BST(LCA)- 无关(仅供参考完整性)
public BinaryTreeNode BstLeastCommonAncestor(int e1, int e2)
        {
            //ensure both elements are there in the bst
            var n1 = this.BstFind(e1, throwIfNotFound: true);
            if(e1 == e2)
            {
                return n1;
            }
            this.BstFind(e2, throwIfNotFound: true);
            BinaryTreeNode leastCommonAcncestor = this._root;
            var iterativeNode = this._root;
            while(iterativeNode != null)
            {
                if((iterativeNode.Element > e1 ) && (iterativeNode.Element > e2))
                {
                    iterativeNode = iterativeNode.Left;
                }
                else if((iterativeNode.Element < e1) && (iterativeNode.Element < e2))
                {
                    iterativeNode = iterativeNode.Right;
                }
                else
                {
                    //i.e; either iterative node is equal to e1 or e2 or in between e1 and e2
                    return iterativeNode;
                }
            }
            //control will never come here
            return leastCommonAcncestor;
        }

单元测试

[TestMethod]
        public void LeastCommonAncestorTests()
        {
            int[] a = { 13, 2, 18, 1, 5, 17, 20, 3, 6, 16, 21, 4, 14, 15, 25, 22, 24 };
            int[] b = { 13, 13, 13, 2, 13, 18, 13, 5, 13, 18, 13, 13, 14, 18, 25, 22};
            BinarySearchTree bst = new BinarySearchTree();
            foreach (int e in a)
            {
                bst.Add(e);
                bst.Delete(e);
                bst.Add(e);
            }
            for(int i = 0; i < b.Length; i++)
            {
                var n = bst.BstLeastCommonAncestor(a[i], a[i + 1]);
                Assert.IsTrue(n.Element == b[i]);
                var n1 = bst.LeastCommonAncestorUsingPaths(a[i], a[i + 1]);
                Assert.IsTrue(n1.Element == b[i]);
                Assert.IsTrue(n == n1);
                var n2 = bst.LeastCommonAncestorUsingRecursion(a[i], a[i + 1]);
                Assert.IsTrue(n2.Element == b[i]);
                Assert.IsTrue(n2 == n1);
                Assert.IsTrue(n2 == n);
            }
        }

0
  1. 前序遍历,直到遇到任何一个节点并保存到目前为止访问的节点。

  2. 中序遍历,在遇到任何一个(提供的两个节点之一)节点时开始保存节点,并保存列表直到下一个节点被遇到。

  3. 后序遍历,在访问了两个节点后开始保存节点...
               A         
      B                  C         
  D       E          F       G       
H   I   J   K      L   M   N   O     

假设 H 和 E 是两个随机节点。

  1. ABDH
  2. HDIBJE
  3. EBLMENOGCA

找到在这三个序列中都出现的第一个节点...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接