Наименьший общий предок двоичного дерева

Это популярный вопрос интервью, и единственная статья, которую я могу найти по этой теме, - это одна из TopCoder. К сожалению для меня, это выглядит слишком сложным с точки зрения ответа на интервью.

нет ли более простого способа сделать это, кроме построения пути к обоим узлам и вывода предка? (Это популярный ответ, но есть вариант вопроса интервью, требующий постоянного пространственного ответа).

6 ответов


постоянный пространственный ответ: (хотя и не обязательно эффективный).

есть функция findItemInPath (int index, int searchId, Node root)

затем повторите от 0 .. глубина дерева, поиску 0-й пункт, 1-й пункт и т. д. в обоих направлениях поиска.

когда вы найдете i таким, что функция возвращает один и тот же результат для обоих, но не для i+1, тогда i-й элемент в пути является самым низким общим предком.


упрощенная (но гораздо менее сложная версия) может быть просто (.NET guy здесь Java немного ржавый, поэтому, пожалуйста, извините синтаксис, но я думаю, вам не придется слишком много настраивать). Вот что я собрал вместе.

class Program
{
    static void Main(string[] args)
    {
        Node node1 = new Node { Number = 1 };
        Node node2 = new Node { Number = 2, Parent = node1 };
        Node node3 = new Node { Number = 3, Parent = node1 };
        Node node4 = new Node { Number = 4, Parent = node1 };
        Node node5 = new Node { Number = 5, Parent = node3 };
        Node node6 = new Node { Number = 6, Parent = node3 };
        Node node7 = new Node { Number = 7, Parent = node3 };
        Node node8 = new Node { Number = 8, Parent = node6 };
        Node node9 = new Node { Number = 9, Parent = node6 };
        Node node10 = new Node { Number = 10, Parent = node7 };
        Node node11 = new Node { Number = 11, Parent = node7 };
        Node node12 = new Node { Number = 12, Parent = node10 };
        Node node13 = new Node { Number = 13, Parent = node10 };

        Node commonAncestor = FindLowestCommonAncestor(node9, node12);

        Console.WriteLine(commonAncestor.Number);
        Console.ReadLine();
    }

    public class Node
    {
        public int Number { get; set; }
        public Node Parent { get; set; }
        public int CalculateNodeHeight()
        {
            return CalculateNodeHeight(this);
        }

        private int CalculateNodeHeight(Node node)
        {
            if (node.Parent == null)
            {
                return 1;
            }

            return CalculateNodeHeight(node.Parent) + 1;
        }
    }

    public static Node FindLowestCommonAncestor(Node node1, Node node2)
    {
        int nodeLevel1 = node1.CalculateNodeHeight();
        int nodeLevel2 = node2.CalculateNodeHeight();

        while (nodeLevel1 > 0 && nodeLevel2 > 0)
        {
            if (nodeLevel1 > nodeLevel2)
            {
                node1 = node1.Parent;
                nodeLevel1--;
            }
            else if (nodeLevel2 > nodeLevel1)
            {
                node2 = node2.Parent;
                nodeLevel2--;
            }
            else
            {
                if (node1 == node2)
                {
                    return node1;
                }

                node1 = node1.Parent;
                node2 = node2.Parent;
                nodeLevel1--;
                nodeLevel2--;
            }
        }

        return null;
    }
}

основная причина, по которой решения статьи более сложны, заключается в том, что она имеет дело с двухэтапной проблемой-предварительной обработкой, а затем запросами, в то время как из вашего вопроса звучит так, как будто вы делаете только один запрос, поэтому предварительная обработка не имеет смысла. Он также имеет дело с произвольными деревьями, а не с бинарными деревьями.

лучший ответ, безусловно, будет зависеть от деталей о дереве. Для многих видов деревьев, сложность будет порядка o(h), где h-это дерево высота. Если у вас есть указатели на родительские узлы, то простой ответ "постоянное пространство", как и в решении Мирко, заключается в том, чтобы найти высоту обоих узлов и сравнить предков одной высоты. Обратите внимание, что это работает для любого дерева с родительскими ссылками, двоичными или нет. Мы можем улучшить решение Мирко, сделав функцию высоты итеративной и отделив циклы" get to the same depth " от основного цикла:

int height(Node n){
  int h=-1;
  while(n!=null){h++;n=n.parent;}
  return h;
}
Node LCA(Node n1, Node n2){
  int discrepancy=height(n1)-height(n2);
  while(discrepancy>0) {n1=n1.parent;discrepancy--;}
  while(discrepancy<0) {n2=n2.parent;discrepancy++;}
  while(n1!=n2){n1=n1.parent();n2=n2.parent();}
  return n1;
}

кавычки вокруг "постоянн-космоса" потому что вообще нам O(log (h)) пространство для хранения высот и разницы между ними (скажем, 3 BigIntegers). Но если вы имеете дело с деревьями с высотами слишком большими, чтобы набивать их в течение длительного времени, у вас, вероятно, есть другие проблемы, о которых нужно беспокоиться, более насущные, чем хранение высоты пары узлов.

Если у вас есть BST, то вы можете легко взять общего предка (usu. начиная с корня) и проверьте своих детей, чтобы увидеть, является ли один из них общим предком:

Node LCA(Node n1, Node n2, Node CA){
 while(true){
  if(n1.val<CA.val & n2.val<CA.val) CA=CA.left;
  else if (n1.val>CA.val & n2.val>CA.val) CA=CA.right;
  else return CA;
 }
}

Как Филипп JF упоминалось, что эта же идея может быть использована в любом дереве для алгоритма с постоянным пространством, но для общего дерева это будет очень медленно, так как неоднократно выяснять, является ли CA.слева или ca.правый общий предок будет повторять много работы, поэтому вы обычно предпочитаете использовать больше места, чтобы сэкономить время. Основным способом сделать этот компромисс будет в основном алгоритм, который вы упомянули (сохранение пути из root).


важно, какое дерево вы используете. Вы всегда можете сказать, является ли узел предком другого узла в постоянном пространстве, а верхний узел всегда является общим предком, поэтому получение самого низкого общего предка в постоянном пространстве просто требует итерации вниз. В двоичном дереве поиска это довольно легко сделать быстро, но это будет работать на любом дереве.

многие различные компромиссы актуальны для этой проблемы, и тип дерева имеет значение. Проблема, как правило, значительно проще, если у вас есть указатели на родительские узлы, а не только на детей (код Mirko использует это)

Смотрите также: http://en.wikipedia.org/wiki/Lowest_common_ancestor


очевидным решением, использующим пространство log(n), (n-количество узлов) является упомянутый вами алгоритм. Вот реализация. В худшем случае это займет O(n) время (представьте, что один из узлов, которые вы ищете общий предок, включает последний узел).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication2
{
    class Node
    {
        private static int counter = 0;
        private Node left = null;
        private Node right = null;
        public int id = counter++;

        static Node constructTreeAux(int depth)
        {
            if (depth == 0)
                return null;
            Node newNode = new Node();
            newNode.left = constructTree(depth - 1);
            newNode.right = constructTree(depth - 1);
            return newNode;
        }

        public static Node constructTree(int depth)
        {
            if (depth == 0)
                return null;
            Node root = new Node();
            root.left = constructTreeAux(depth - 1);
            root.right = constructTreeAux(depth - 1);
            return root;
        }

        private List<Node> findPathAux(List<Node> pathSoFar, int searchId)
        {
            if (this.id == searchId)
            {
                if (pathSoFar == null)
                    pathSoFar = new List<Node>();
                pathSoFar.Add(this);
                return pathSoFar;
            }
            if (left != null)
            {
                List<Node> result = left.findPathAux(null, searchId);
                if (result != null)
                {
                    result.Add(this);
                    return result;
                }
            }
            if (right != null)
            {
                List<Node> result = right.findPathAux(null, searchId);
                if (result != null)
                {
                    result.Add(this);
                    return result;
                }
            }
            return null;
        }

        public static void printPath(List<Node> path)
        {
            if (path == null)
            {
                Console.Out.WriteLine(" empty path ");
                return;
            }
            Console.Out.Write("[");
            for (int i = 0; i < path.Count; i++)
                Console.Out.Write(path[i] + " ");
            Console.Out.WriteLine("]");
        }

        public override string ToString()
        {
            return id.ToString();
        }

        /// <summary>
        /// Returns null if no common ancestor, the lowest common ancestor otherwise.
        /// </summary>
        public Node findCommonAncestor(int id1, int id2)
        {
            List<Node> path1 = findPathAux(null, id1);
            if (path1 == null)
                return null;
            path1 = path1.Reverse<Node>().ToList<Node>();
            List<Node> path2 = findPathAux(null, id2);
            if (path2 == null)
                return null;
            path2 = path2.Reverse<Node>().ToList<Node>();
            Node commonAncestor = this;
            int n = path1.Count < path2.Count? path1.Count : path2.Count;
            printPath(path1);
            printPath(path2);
            for (int i = 0; i < n; i++)
            {
                if (path1[i].id == path2[i].id)
                    commonAncestor = path1[i];
                else
                    return commonAncestor;
            }          
            return commonAncestor;
        }

        private void printTreeAux(int depth)
        {
            for (int i = 0; i < depth; i++)
                Console.Write("  ");
            Console.WriteLine(id);
            if (left != null)
                left.printTreeAux(depth + 1);
            if (right != null)
                right.printTreeAux(depth + 1);
        }

        public void printTree()
        {
            printTreeAux(0);
        }
        public static void testAux(out Node root, out Node commonAncestor, out int id1, out int id2)
        {
            Random gen = new Random();
            int startid = counter;
            root = constructTree(5);
            int endid = counter;

            int offset = gen.Next(endid - startid);
            id1 = startid + offset;
            offset = gen.Next(endid - startid);
            id2 = startid + offset;
            commonAncestor = root.findCommonAncestor(id1, id2);

        }
        public static void test1()
        {
            Node root = null, commonAncestor = null;
            int id1 = 0, id2 = 0;
           testAux(out root, out commonAncestor, out id1, out id2);
            root.printTree();
             commonAncestor = root.findCommonAncestor(id1, id2);
            if (commonAncestor == null)
                Console.WriteLine("Couldn't find common ancestor for " + id1 + " and " + id2);
            else
                Console.WriteLine("Common ancestor for " + id1 + " and " + id2 + " is " + commonAncestor.id);
        }
    }
}

описанный подход снизу вверх здесь - Это O(n) время, O (1) пространственный подход:

http://www.leetcode.com/2011/07/lowest-common-ancestor-of-a-binary-tree-part-i.html

Node *LCA(Node *root, Node *p, Node *q) {
  if (!root) return NULL;
  if (root == p || root == q) return root;
  Node *L = LCA(root->left, p, q);
  Node *R = LCA(root->right, p, q);
  if (L && R) return root;  // if p and q are on both sides
  return L ? L : R;  // either one of p,q is on one side OR p,q is not in L&R subtrees
}