Нахождение общего предка в бинарном дереве - PullRequest
7 голосов
/ 30 мая 2011

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


Мой ответ:

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

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

Ответы [ 10 ]

7 голосов
/ 31 августа 2011

Может быть, глупый подход:

Генерировать путь от каждого узла к корню, сохраняя его в виде строки «L» и «R».

Обратно эти строки.Возьмите самый длинный общий префикс - теперь у вас есть путь к общему предку.

6 голосов
/ 30 августа 2011

Установить указатель на оба случайных узла.Найдите глубину каждого узла, пройдя вверх и посчитав расстояние от корневого узла.Затем снова установите указатель на оба узла.Для более глубокого узла перемещайтесь вверх, пока оба указателя не окажутся на одной и той же глубине.Затем перемещайтесь вверх для обоих узлов, пока указатели не укажут на один и тот же узел.Это узел-предок.

Под «перемещением вверх» я подразумеваю перемещение указателя на родительский узел текущего узла.

Правка для уточнения: Ключевая идеяв том, что, когда оба узла находятся на одной и той же глубине, вы можете очень быстро найти общего родителя просто путем обхода.Таким образом, вы поднимаетесь на нижнюю, пока оба не окажутся на одной и той же глубине, а затем двигаетесь вверх.Извините, я на самом деле не знаю C, или я бы написал код, но этот алгоритм должен ответить на ваш вопрос.

Изменить еще раз: И мой метод работает в O (log (n))время и O (1) памяти.

Другое редактирование: O (log (n)) в сбалансированном дереве.Наихудшая производительность O (n) для несбалансированного дерева.Спасибо @ DaveCahill

3 голосов
/ 05 февраля 2012

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

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 голосов
/ 30 мая 2011

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

EDIT1:

Вот схема

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);
}

UPDATE

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

Приведенный ниже алгоритм найдет общих предков, а не только родителей.

Я думаю, что следующий алгоритм будет работать:

Сделайте обход по порядку двоичного дерева и найдите для случайного узла 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;
}

Я думаю, что это будет работать правильно, хотя мне еще предстоит доказать правильность алгоритма. Существует один недостаток: если один узел является дочерним по отношению к другому узлу, он будет печатать только узел, являющийся родительским для другого, вместо печати родительского из них. Если один случайный узел является предком другого случайного узла, тогда вместо печати случайного узла предка будет напечатан его родитель. В случае, когда один из случайных узлов является корневым, он ничего не печатает, так как он всегда является предком другого случайного узла, и, следовательно, их общего предка не существует. В этом особом случае функция вернет 0x03 в main и ее можно обнаружить.

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

UPDATE

Вот некоторые обсуждения режима: Как найти наименьшего общего предка двух узлов в любом двоичном дереве?

1 голос
/ 30 августа 2011

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

0 голосов
/ 14 июля 2014

Вот два подхода в c # (.net) (оба обсуждались выше) для справки:

  1. Рекурсивная версия нахождения LCA в двоичном дереве (O (N) - поскольку посещается не более каждого узла) (Основными точками решения является LCA - (a) Единственный узел в двоичном дереве, где оба элемента находятся по обе стороны от поддеревьев (слева и справа) - это LCA. (b) И также не имеет значения, какой узел присутствует с обеих сторон - изначально я пытался сохранить эту информацию, и, очевидно, рекурсивная функция стала настолько запутанной. Как только я понял это, она стала очень элегантной.

  2. Поиск в обоих узлах (O (N)) и отслеживание путей (использует дополнительное пространство - поэтому, # 1, вероятно, лучше, даже если пространство, вероятно, ничтожно, если двоичное дерево хорошо сбалансировано, как тогда дополнительное потребление памяти будет только в O (log (N)).

    так, чтобы пути сравнивались (по сути, аналогично принятому ответу - но пути вычисляются, предполагая, что указательный узел отсутствует в узле двоичного дерева)

  3. Только для завершения ( не относится к вопросу ), LCA в BST (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 голосов
/ 25 сентября 2012

Привет это вернет наименьшее значение узла предка, где передаются корень дерева и 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 голосов
/ 15 января 2012
  1. Обход предзаказа, если не встречен 1 узел, и сохраните посещенные узлы до сих пор.

  2. Обход по порядку, начните сохранение узлов, когда они будут 1(из двух предоставленных узлов) узел встречается и сохраняйте список до тех пор, пока не будет встречен следующий узел.

  3. обход после заказа, начните сохранять узлы, когда оба узла были посещены ...
A         
B                  C         
D       E          F       G       
H   I   J   K      L   M   N   O     

Предположим, H и E - два случайных узла.

  1. ABDH
  2. HDIBJE
  3. EBLMENOGCA

Найти первый общий узел во всех трех ...

0 голосов
/ 30 августа 2011

псевдокод:

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 - глубина узла 1, поэтому это O (n). Второй цикл выполняется m раз, где m на глубине node2. Поиск во временном списке (в худшем случае) n. Итак, второй цикл - O (m * n), и он доминирует, поэтому функция выполняется в O (m * n).

Если вы используете хорошую структуру данных набора (например, хеш-таблицу) для временного пространства вместо списка, вы можете сократить поиск до (обычно) O (1), не увеличивая стоимость добавления узлов во временную область. , Это уменьшает время нашей функции до O (м).

Требуемое пространство равно 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 голосов
/ 30 августа 2011

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

            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;
     }
...