Як знайти найнижчого загального предка двох вузлів у будь-якому бінарному дереві?


187

Бінарне дерево тут не обов'язково може бути двійковим деревом пошуку.
Структуру можна сприймати як -

struct node {
    int data;
    struct node *left;
    struct node *right;
};

Максимальне рішення, з яким я міг працювати з другом, було щось подібне -
розглянемо це бінарне дерево :

Бінарне дерево

Врожайність міжрегіональних врожаїв - 8, 4, 9, 2, 5, 1, 6, 3, 7

А урожай після обходу - 8, 9, 4, 5, 2, 6, 7, 3, 1

Так, наприклад, якщо ми хочемо знайти спільного предка вузлів 8 і 5, то ми складемо список усіх вузлів, які знаходяться між 8 і 5 в обхідному дереві, що в цьому випадку відбувається [4, 9 , 2]. Потім ми перевіряємо, який вузол у цьому списку з’являється останнім у проїзді після впорядкування, який є 2. Отже, загальним предком для 8 та 5 є 2.

Складність цього алгоритму, я вважаю, є O (n) (O (n) для переходу в інтервальному / післяпорядковому порядку, решта кроків знову O (n), оскільки вони є не що інше, як прості ітерації в масивах). Але є великий шанс, що це неправильно. :-)

Але це дуже грубий підхід, і я не впевнений, чи вийде з ладу для якоїсь справи. Чи є якесь інше (можливо більш оптимальне) рішення цієї проблеми?


6
З цікавості, яке практичне використання цього?
Девід Брунелль

19
@David: відповідь на запит LCA дуже корисна. LCA + Suffix tree = потужні алгоритми, пов’язані з рядками.

44
І коли я задав подібне запитання, він проголосив коментарі, як його запитання про інтерв'ю. Подвійність SO? :(
some_other_guy

5
@Siddant +1 для деталей, наведених у запитанні. :)
amod

5
@DavidBrunelle Одне практичне застосування обчислення LCA: це важливий розрахунок під час надання веб-сторінок, зокрема при обчисленні таблиць стилів каскадного стилю (CSS), застосовних до певного елемента DOM.
zc22

Відповіді:


74

Нік Джонсон є правильним , що алгоритм O (п) складність краще , що ви можете зробити , якщо у вас немає батьківських покажчиків.) Для простої рекурсивної версії цього алгоритму код , наведений в пості KINDING в якому працює в O (п) .

Але майте на увазі, що якщо у ваших вузлах є батьківські вказівники, можливий вдосконалений алгоритм. Для обох вузлів, про які йдеться, побудуйте список, що містить шлях від кореня до вузла, починаючи з вузла та передньою вставкою батьківського.

Отже для 8 у вашому прикладі ви отримуєте (показуючи кроки): {4}, {2, 4}, {1, 2, 4}

Зробіть те ж саме для іншого відповідного вузла, у результаті чого (кроки не показані): {1, 2}

Тепер порівняйте два списки, які ви склали, шукаючи перший елемент, де список відрізняється, або останній елемент одного зі списків, що б не відбулося першим.

Цей алгоритм вимагає часу O (h), де h - висота дерева. У гіршому випадку O (h) еквівалентний O (n), але якщо дерево врівноважене, це лише O (log (n)). Для цього також потрібен O ​​(h) простір. Можлива вдосконалена версія, що використовує лише постійний простір, з кодом, показаним у публікації CEGRD


Незалежно від того, як побудовано дерево, якщо це буде операція, яку ви виконуєте багато разів на дереві, не змінюючи його між ними, існують інші алгоритми, які можна використовувати, для яких потрібна підготовка O (n) [лінійного] часу, але потім знайти будь-який пара займає лише O (1) [постійний] час. Для посилань на ці алгоритми див. Найнижчу загальну сторінку проблеми предків у Вікіпедії . (Подяка Джейсону за первісну публікацію цього посилання)


1
Це виконує завдання, якщо вказано батьківський вказівник. Вузли на дереві - це структура, яку я вказав у своєму запитанні - лише лівий / правий дочірні вказівники, жоден батьківський вказівник. Чи є рішення O (log (n)), якщо немає батьківського вказівника, і дерево не є двійковим деревом пошуку, а є лише бінарним деревом?
Сіддхант

2
Якщо у вас немає конкретного способу пошуку шляху між батьківським і заданим вузлом, то в середньому знадобиться O (n) час. Це унеможливить наявність часу O (log (n)). Однак разова вартість O (n), якщо пошук пари O (1) може бути найкращим варіантом у будь-якому випадку, якщо ви збиралися виконувати цю операцію багато разів, не змінюючи дерево між ними. В іншому випадку, якщо це можливо, слід додати батьківський вказівник. Це може зробити досить багато потенційних алгоритмів швидше, але я впевнений, що він не змінює порядок жодного існуючого алгоритму. Сподіваюся, це допомагає.
Кевін Каткарт

1
цей підхід може бути зроблений з допомогою O (1) пам'яті - см Artelius (і інших) рішення в stackoverflow.com/questions/1594061 / ...
Том Sirgedas

@Tom: Дійсно, це могло б обмежити складність пам'яті до O (1) для алгоритму, заснованого на списку. Очевидно, що це означає повторення через саме дерево один раз для кожної сторони, щоб отримати глибину вузлів, а потім (частково) вдруге знайти спільного предка. Час O (h) та простір O (1) очевидно оптимальні для випадку наявності батьківських вказівників, а також не попереднього обчислення O (n).
Кевін Кеткарт

1
@ALBI O(h)є лише в тому випадку, O(log(n))якщо дерево збалансоване. Будь-яке дерево, будь то двійкове чи ні, якщо у вас є батьківські вказівники, ви можете O(h)вчасно визначити шлях від листа до кореня , просто дотримуючись батьківський вказівник до hразів. Це дає вам шлях від листя до кореня. Якщо шляхи зберігаються як стек, то ітерація стека дає вам шлях від кореня до листа. Якщо вам не вистачає батьківських покажчиків і не маєте особливої ​​структури до дерева, знаходження шляху від кореня до листя дійсно потребує O(n)часу.
Кевін Каткарт

108

Починаючи з rootвузла і рухаючись вниз, якщо ви знайдете будь-який вузол, у якого є pабо qйого пряма дитина, то це LCA. (Редагування - це повинно бути , якщо pі qце значення вузла, повернути його В іншому випадку він зазнає невдачі , коли один з. , pАбо qє прямим нащадком іншого.)

В іншому випадку, якщо ви знайдете вузол з pйого правою (або лівою) піддією, а qв лівій (або правій) піддією, то це LCA.

Фіксований код виглядає так:

treeNodePtr findLCA(treeNodePtr root, treeNodePtr p, treeNodePtr q) {

        // no root no LCA.
        if(!root) {
                return NULL;
        }

        // if either p or q is the root then root is LCA.
        if(root==p || root==q) {
                return root;
        } else {
                // get LCA of p and q in left subtree.
                treeNodePtr l=findLCA(root->left , p , q);

                // get LCA of p and q in right subtree.
                treeNodePtr r=findLCA(root->right , p, q);

                // if one of p or q is in leftsubtree and other is in right
                // then root it the LCA.
                if(l && r) {
                        return root;
                }
                // else if l is not null, l is LCA.
                else if(l) {
                        return l;
                } else {
                        return r;
                }
        }
}

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

treeNodePtr findLCA(treeNodePtr root, treeNodePtr p, treeNodePtr q) {

        // no root no LCA.
        if(!root) {
                return NULL;
        }

        // if either p or q is direct child of root then root is LCA.
        if(root->left==p || root->left==q || 
           root->right ==p || root->right ==q) {
                return root;
        } else {
                // get LCA of p and q in left subtree.
                treeNodePtr l=findLCA(root->left , p , q);

                // get LCA of p and q in right subtree.
                treeNodePtr r=findLCA(root->right , p, q);

                // if one of p or q is in leftsubtree and other is in right
                // then root it the LCA.
                if(l && r) {
                        return root;
                }
                // else if l is not null, l is LCA.
                else if(l) {
                        return l;
                } else {
                        return r;
                }
        }
}

Кодекс в дії


2
елегантне рішення, але корінь == p || root == q => біт повернення кореня здається надто оптимістичним. Що робити, якщо виявиться, що корінь є p / q, але інший шуканий вузол насправді не в дереві?
Ян Дуркан

15
Я думаю, що цей код виходить з ладу, коли p або q є значенням, якого немає у двійковому дереві. Маю рацію? Наприклад LCA (8,20). код ур повертається 8. але 20 не присутній у двійковому дереві
javaMan

3
Яка вартість цього рішення? Це ефективно? Здається, він продовжує пошук навіть після того, як знайшов і p, і q. Це через можливість того, що p і q не можуть бути унікальними у дереві, оскільки це не BST і може містити дублікати?
MikeB

3
@MikeB, це рішення, безумовно, O (n), оскільки ви переходите кожен вузол лише один раз у гіршому випадку. Пітер Лі, це найефективніше, що можна зробити, не використовуючи батьківських вказівників. Чи маєте ви краще рішення?
gsingh2011

8
перше недосконале рішення слід видалити, щоб воно не відволікало
Zinan Xing

50

Ось робочий код JAVA

public static Node LCA(Node root, Node a, Node b) {
   if (root == null) {
       return null;
   }

   // If the root is one of a or b, then it is the LCA
   if (root == a || root == b) {
       return root;
   }

   Node left = LCA(root.left, a, b);
   Node right = LCA(root.right, a, b);

   // If both nodes lie in left or right then their LCA is in left or right,
   // Otherwise root is their LCA
   if (left != null && right != null) {
      return root;
   }

   return (left != null) ? left : right; 
}

4
Це не працює, коли у дереві не існує вузла.
Пратік Хадлоя,

Ви б оптимізували свій код, якщо дане дерево є BST?
Мона Джалал

1
"Якщо корінь є одним із a або b, то це LCA." це може бути неправдою. Що ви знаєте на даний момент, це те, що вам не потрібно перевіряти жодного з його дітей, щоб знайти ДМС. Це трапляється тому, що згодом ми можемо перевірити, чи є для батьків-кореневих кореневих файлів відповідність обох гілок (LCA є батьківською) або лише одна з них (у цьому випадку це може бути LCA, або ще більшим предком може бути LCA ).
andresp

28

Надані відповіді поки що використовують рекурсію або зберігають, наприклад, шлях у пам'яті.

Обидва ці підходи можуть провалитися, якщо у вас дуже глибоке дерево.

Ось мій погляд на це питання. Коли ми перевіряємо глибину (відстань від кореня) обох вузлів, якщо вони рівні, то ми можемо сміливо рухатися вгору від обох вузлів до загального предка. Якщо одна з глибин більша, то нам слід рухатися вгору від глибшого вузла, залишаючись в іншому.

Ось код:

findLowestCommonAncestor(v,w):
  depth_vv = depth(v);
  depth_ww = depth(w);

  vv = v; 
  ww = w;

  while( depth_vv != depth_ww ) {
    if ( depth_vv > depth_ww ) {
      vv = parent(vv);
      depth_vv--;
    else {
      ww = parent(ww);
      depth_ww--;
    }
  }

  while( vv != ww ) {
    vv = parent(vv);
    ww = parent(ww);
  }

  return vv;    

Часова складність цього алгоритму становить: O (n). Просторова складність цього алгоритму становить: O (1).

Щодо обчислення глибини, ми можемо спочатку згадати визначення: Якщо v - корінь, глибина (v) = 0; В іншому випадку глибина (v) = глибина (батьківський (v)) + 1. Ми можемо обчислити глибину наступним чином:

depth(v):
  int d = 0;
  vv = v;
  while ( vv is not root ) {
    vv = parent(vv);
    d++;
  }
  return d;

6
Бінарні дерева, як правило, не мають посилання на батьківський елемент. Додавання батьківського посилання може бути здійснено без жодних питань, але я вважаю, що O (n) допоміжний простір.
Джон Курлак

У цьому рішенні є тонке припущення. Якщо один вузол є прямим або непрямим батьківським іншим (тобто, більш глибокий вузол знаходиться у дереві, вкоріненому на менший вузол), це рішення повертає батьківський результат меншого вузла як результат. Залежно від того, як ви визначаєте найнижчого загального предка, це може бути не тим, чого ви хочете. Деякі визначення потребуватимуть, що найменший вузол буде батьківським. У цьому випадку вам потрібно буде відстежити, який є більш дрібний вузол, і повернути його.
Шрікант

8

Ну, цей вид залежить від того, як структуровано Ваше Бінарне Дерево. Імовірно, у вас є якийсь спосіб знайти потрібний вузол листя за корінням дерева - просто застосуйте це до обох значень, поки гілки, які ви вибрали, не розходяться.

Якщо у вас немає способу знайти потрібний лист з урахуванням кореня, то ваше єдине рішення - як при звичайній роботі, так і для пошуку останнього загального вузла - це грубої пошук дерева.


8

Це можна знайти за адресою: - http://goursaha.freeoda.com/DataStructure/LowestCommonAncestor.html

 tree_node_type *LowestCommonAncestor(
 tree_node_type *root , tree_node_type *p , tree_node_type *q)
 {
     tree_node_type *l , *r , *temp;
     if(root==NULL)
     {
        return NULL;
     }

    if(root->left==p || root->left==q || root->right ==p || root->right ==q)
    {
        return root;
    }
    else
    {
        l=LowestCommonAncestor(root->left , p , q);
        r=LowestCommonAncestor(root->right , p, q);

        if(l!=NULL && r!=NULL)
        {
            return root;
        }
        else
        {
        temp = (l!=NULL)?l:r;
        return temp;
        }
    }
}

чи можете ви мені скажіть, як буде поводитися ваш код, якщо p присутній, але q взагалі не присутній у дереві? Аналогічно і p, і q відсутні. Дякую!!!
Проба

Який великий О в плані часу? Я думаю, що це O (n * log (n)), два повільних.
Пітер Лі


6

Щоб дізнатися спільного предка двох вузлів: -

  • Знайдіть заданий вузол Node1 у дереві, використовуючи двійковий пошук і збережіть усі вузли, відвідані в цьому процесі, у масиві скажімо A1. Час - O (вхід), пробіл - O (вхід)
  • Знайдіть заданий Node2 в дереві, використовуючи двійковий пошук і збережіть усі вузли, відвідані в цьому процесі, у масиві, що каже A2. Час - O (вхід), пробіл - O (вхід)
  • Якщо список А1 або список А2 порожній, то одного вузла не існує, тому спільного предка немає.
  • Якщо список А1 та А2-список не порожній, перегляньте його, доки не знайдете невідповідний вузол. Як тільки ви знайдете такий вузол, тоді вузол до цього є загальним предком.

Це буде працювати для дерева двійкового пошуку.


2
Він чітко заявив, що дерево НЕ обов'язково BST.
Пітер Лі

@ Петер Лі - Вищеописана логіка працюватиме навіть для будь-якого бінарного дерева з простою зміною. Замість двійкового пошуку даних вузлів застосуйте лінійний пошук (тобто будь-який обхід, але має бути однаковим для обох випадків). Час виконання поза курсом буде O (n) замість O (logn). Насправді цей альго є найбільш надійним, коли батьківський вказівник недоступний. Рукусивний алгоритм, заданий багатьма (а саме "кодидант") не буде працювати, коли один із заданих вузлів не належить дереву)
KGhatak


3

Нижче рекурсивний алгоритм буде працювати в O (log N) для збалансованого бінарного дерева. Якщо будь-який з вузлів, переданих у функцію getLCA (), такий же, як і корінь, то коренем буде LCA, і не буде необхідності виконувати повторне обговорення.

Тестові справи. [1] Обидва вузли n1 та n2 знаходяться в дереві та розташовані по обидва боки від їхнього батьківського вузла. [2] Або вузол n1, або n2 - корінь, LCA - корінь. [3] Лише n1 або n2 є у дереві, LCA буде або кореневим вузлом лівого піддерева кореня дерева, або LCA буде кореневим вузлом правого піддерева кореня дерева.

[4] Ні n1, ні n2 не є в дереві, немає LCA. [5] І n1, і n2 знаходяться в прямій прямій поруч, LCA буде або n1, або n2, яка коли-небудь закривається до кореня дерева.

//find the search node below root
bool findNode(node* root, node* search)
{
    //base case
    if(root == NULL)
        return false;

    if(root->val == search->val)
        return true;

    //search for the node in the left and right subtrees, if found in either return true
    return (findNode(root->left, search) || findNode(root->right, search));
}

//returns the LCA, n1 & n2 are the 2 nodes for which we are
//establishing the LCA for
node* getLCA(node* root, node* n1, node* n2)
{
    //base case
    if(root == NULL)
        return NULL;

    //If 1 of the nodes is the root then the root is the LCA
    //no need to recurse.
    if(n1 == root || n2 == root)
        return root;

    //check on which side of the root n1 and n2 reside
    bool n1OnLeft = findNode(root->left, n1);
    bool n2OnLeft = findNode(root->left, n2);

    //n1 & n2 are on different sides of the root, so root is the LCA
    if(n1OnLeft != n2OnLeft)
        return root;

    //if both n1 & n2 are on the left of the root traverse left sub tree only
    //to find the node where n1 & n2 diverge otherwise traverse right subtree
    if(n1OnLeft)
        return getLCA(root->left, n1, n2);
    else
        return getLCA(root->right, n1, n2);
}

3

Просто сходьте зі всього дерева до rootтих пір, як обидва задані вузли, скажімо, pіq для яких потрібно знайти Предка, знаходяться в одному під дереві (тобто їх значення обидва менші або обидва більші, ніж корені).

Це прямує прямо від кореня до найменшого спільного предка, не дивлячись на решту дерева, тож це дуже швидко, як це стає. Кілька способів це зробити.

Ітеративний, O (1) простір

Пітон

def lowestCommonAncestor(self, root, p, q):
    while (root.val - p.val) * (root.val - q.val) > 0:
        root = (root.left, root.right)[p.val > root.val]
    return root

Java

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    while ((root.val - p.val) * (root.val - q.val) > 0)
        root = p.val < root.val ? root.left : root.right;
    return root;
}

у випадку переповнення, я б зробив (root.val - (довгий) p.val) * (root.val - (довгий) q.val)

Рекурсивний

Пітон

def lowestCommonAncestor(self, root, p, q):
    next = p.val < root.val > q.val and root.left or \
           p.val > root.val < q.val and root.right
    return self.lowestCommonAncestor(next, p, q) if next else root

Java

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    return (root.val - p.val) * (root.val - q.val) < 1 ? root :
           lowestCommonAncestor(p.val < root.val ? root.left : root.right, p, q);
}

2
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
}

2

Розгляньте це дерево введіть тут опис зображення

Якщо ми зробимо післяпорядковий та передпорядковий обхід і знайдемо першого, що виникає спільного попередника та наступника, ми отримаємо спільного предка.

порядок => 0,2,1,5,4,6,3,8,10,11,9,14,15,13,12,7 попередній замовлення => 7,3,1,0,2,6,4 , 5,12,9,8,11,10,13,15,14

  • наприклад: 1

Найменше загальний предок 8,11

у післяпорядку маємо => 9,14,15,13,12,7 після 8 та 11 у передпорядці маємо => 7,3,1,0,2,6,4,5,12,9 до 8 та 11

9 - це перше загальне число, яке виникає після 8 та 11 після замовлення та перед 8 та 11 перед замовленням, отже, 9 - це відповідь

  • напр .: 2

Найменше загальний предок 5,10

11,9,14,15,13,12,7 в післяпорядку 7,3,1,0,2,6,4 в передпорядкуванні

7 - це перше число, яке виникає після 5,10 після замовлення і перед 5,10 в передпорядкуванні, отже, 7 - це відповідь


2

Якщо це повне бінарне дерево з дітьми вузла x як 2 * x і 2 * x + 1, то є більш швидкий спосіб зробити це

int get_bits(unsigned int x) {
  int high = 31;
  int low = 0,mid;
  while(high>=low) {
    mid = (high+low)/2;
    if(1<<mid==x)
      return mid+1;
    if(1<<mid<x) {
      low = mid+1;
    }
    else {
      high = mid-1;
    }
  }
  if(1<<mid>x)
    return mid;
  return mid+1;
}

unsigned int Common_Ancestor(unsigned int x,unsigned int y) {

  int xbits = get_bits(x);
  int ybits = get_bits(y);
  int diff,kbits;
  unsigned int k;
  if(xbits>ybits) {
    diff = xbits-ybits;
    x = x >> diff;
  }
  else if(xbits<ybits) {
    diff = ybits-xbits;
    y = y >> diff;
  }
  k = x^y;
  kbits = get_bits(k);
  return y>>kbits;  
}

Як це працює

  1. отримати біти, необхідні для представлення x & y, який використовує двійковий пошук O (журнал (32))
  2. загальний префікс двійкового позначення x & y є загальним предком
  3. залежно від того, який розмір представлений більшим числом бітів, доводиться до одного біта k >> diff
  4. k = x ^ y erazes поширений префікс x & y
  5. знайти біти, що представляють решту суфіксу
  6. shift x або y суфіксними бітами, щоб отримати спільний префікс, який є загальним предком.

Це працює, оскільки в основному ділимо велике число на два рекурсивно, поки обидва числа не будуть рівними. Це число є загальним предком. Ділення - це фактично справна операція зсуву. Тому нам потрібно знайти загальний префікс з двох чисел, щоб знайти найближчого предка


2

У масштабі ви можете:

  abstract class Tree
  case class Node(a:Int, left:Tree, right:Tree) extends Tree
  case class Leaf(a:Int) extends Tree

  def lca(tree:Tree, a:Int, b:Int):Tree = {
    tree match {
      case Node(ab,l,r) => {
        if(ab==a || ab ==b) tree else {
          val temp = lca(l,a,b);
          val temp2 = lca(r,a,b);
          if(temp!=null && temp2 !=null)
            tree
          else if (temp==null && temp2==null)
            null
          else if (temp==null) r else l
        }

      }
      case Leaf(ab) => if(ab==a || ab ==b) tree else null
    }
  }

1
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null || root == p || root == q){
            return root;
        }
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);
        return left == null ? right : right == null ? left : root;
    }

0

Ось спосіб C ++ зробити це. Спробували зберегти алгоритм якомога легше для розуміння:

// Assuming that `BinaryNode_t` has `getData()`, `getLeft()` and `getRight()`
class LowestCommonAncestor
{
  typedef char type;    
  // Data members which would behave as place holders
  const BinaryNode_t* m_pLCA;
  type m_Node1, m_Node2;

  static const unsigned int TOTAL_NODES = 2;

  // The core function which actually finds the LCA; It returns the number of nodes found
  // At any point of time if the number of nodes found are 2, then it updates the `m_pLCA` and once updated, we have found it!
  unsigned int Search (const BinaryNode_t* const pNode)
  {
    if(pNode == 0)
      return 0;

    unsigned int found = 0;

    found += (pNode->getData() == m_Node1);
    found += (pNode->getData() == m_Node2);

    found += Search(pNode->getLeft()); // below condition can be after this as well
    found += Search(pNode->getRight());

    if(found == TOTAL_NODES && m_pLCA == 0)
      m_pLCA = pNode;  // found !

    return found;
  }

public:
  // Interface method which will be called externally by the client
  const BinaryNode_t* Search (const BinaryNode_t* const pHead,
                              const type node1,
                              const type node2)
  {
    // Initialize the data members of the class
    m_Node1 = node1;
    m_Node2 = node2;
    m_pLCA = 0;

    // Find the LCA, populate to `m_pLCANode` and return
    (void) Search(pHead);
    return m_pLCA;
  }
};

Як ним користуватися:

LowestCommonAncestor lca;
BinaryNode_t* pNode = lca.Search(pWhateverBinaryTreeNodeToBeginWith);
if(pNode != 0)
  ...

0

Найпростіший спосіб знайти найменшого загального предка - це використовувати наступний алгоритм:

Огляньте кореневий вузол

якщо значення1 та значення2 суворо менші за значення в кореневому вузлі 
    Вивчіть ліве піддерево
інакше, якщо value1 та value2 суворо перевищують значення у кореневому вузлі 
    Вивчіть праве піддерево
ще
    повернути корінь
public int LCA(TreeNode root, int value 1, int value 2) {
    while (root != null) {
       if (value1 < root.data && value2 < root.data)
           return LCA(root.left, value1, value2);
       else if (value2 > root.data && value2 2 root.data)
           return LCA(root.right, value1, value2);
       else
           return root
    }

    return null;
} 

6
НЕ БЕСТ!
Пітер Лі

0

Я знайшов рішення

  1. Візьміть замовлення
  2. Візьміть попереднє замовлення
  3. Візьміть поштовий замовлення

Залежно від 3 об’їздів, ви можете вирішити, хто є LCA. Від LCA знайдіть відстань обох вузлів. Додайте ці дві відстані, що є відповіддю.


0

Ось що я думаю,

  1. Знайдіть маршрут для кулакового вузла, збережіть його до arr1.
  2. Почніть пошук маршруту для 2-го вузла, виконуючи це, перевіряйте кожне значення від кореня до arr1.
  3. час, коли значення відрізняється, вихід. Старе відповідне значення - це LCA.

Складність: крок 1: O (n), крок 2 = ~ O (n), загальний = ~ O (n).


0

Ось два підходи в c # (.net) (обидва обговорені вище) для довідок:

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

  2. Пошук обох вузлів (O (N)) та відстеження шляхів (використовує додатковий простір - так, №1, мабуть, перевершує навіть думку, що простір, мабуть, мізерно, якщо бінарне дерево добре збалансоване, тому що додаткове споживання пам'яті буде тільки в O (журнал (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

Якщо хтось цікавиться псевдокодом (для домашніх робіт в університеті), ось один.

GETLCA(BINARYTREE BT, NODE A, NODE  B)
IF Root==NIL
    return NIL
ENDIF

IF Root==A OR root==B
    return Root
ENDIF

Left = GETLCA (Root.Left, A, B)
Right = GETLCA (Root.Right, A, B)

IF Left! = NIL AND Right! = NIL
    return root
ELSEIF Left! = NIL
    Return Left
ELSE
    Return Right
ENDIF

0

Хоча на це вже відповіли, це мій підхід до цієї проблеми за допомогою мови програмування на С. Хоча код показує двійкове дерево пошуку (що стосується insert ()), але алгоритм працює і для бінарного дерева. Ідея полягає в тому, щоб перейти через усі вузли, що лежать від вузла А до вузла В, в обхідному проході, знайти їх індекси в проходженні після замовлення. Вузол з максимальним індексом в обході після замовлення є найнижчим загальним предком.

Це робочий код С для реалізації функції пошуку найнижчого загального предка у бінарному дереві. Я також надаю всі функції утиліти тощо, але перейдіть до CommonAncestor () для швидкого розуміння.

#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <math.h>

static inline int min (int a, int b)
{
    return ((a < b) ? a : b);
}
static inline int max (int a, int b)
{
    return ((a > b) ? a : b);
}

typedef struct node_ {
    int value;
    struct node_ * left;
    struct node_ * right;
} node;

#define MAX 12

int IN_ORDER[MAX] = {0};
int POST_ORDER[MAX] = {0};

createNode(int value) 
{
    node * temp_node = (node *)malloc(sizeof(node));
    temp_node->left = temp_node->right = NULL;
    temp_node->value = value;
    return temp_node;
}

node *
insert(node * root, int value)
{
    if (!root) {
        return createNode(value);
    }

    if (root->value > value) {
        root->left = insert(root->left, value);
    } else {
        root->right = insert(root->right, value);
    }

    return root;
}


/* Builds inorder traversal path in the IN array */
void
inorder(node * root, int * IN)
{
    static int i = 0;

    if (!root) return;

    inorder(root->left, IN);
    IN[i] = root->value;
    i++;
    inorder(root->right, IN);
}

/* Builds post traversal path in the POST array */

void
postorder (node * root, int * POST)
{
    static int i = 0;

    if (!root) return;

    postorder(root->left, POST);
    postorder(root->right, POST);
    POST[i] = root->value;
    i++;
}


int
findIndex(int * A, int value)
{
    int i = 0;
    for(i = 0; i< MAX; i++) {
        if(A[i] == value) return i;
    }
}
int
CommonAncestor(int val1, int val2)
{
    int in_val1, in_val2;
    int post_val1, post_val2;
    int j=0, i = 0; int max_index = -1;

    in_val1 = findIndex(IN_ORDER, val1);
    in_val2 = findIndex(IN_ORDER, val2);
    post_val1 = findIndex(POST_ORDER, val1);
    post_val2 = findIndex(POST_ORDER, val2);

    for (i = min(in_val1, in_val2); i<= max(in_val1, in_val2); i++) {
        for(j = 0; j < MAX; j++) {
            if (IN_ORDER[i] == POST_ORDER[j]) {
                if (j > max_index) {
                    max_index = j;
                }
            }
        }
    }
    printf("\ncommon ancestor of %d and %d is %d\n", val1, val2, POST_ORDER[max_index]);
    return max_index;
}
int main()
{
    node * root = NULL; 

    /* Build a tree with following values */
    //40, 20, 10, 30, 5, 15, 25, 35, 1, 80, 60, 100
    root = insert(root, 40);
    insert(root, 20);
    insert(root, 10);
    insert(root, 30);
    insert(root, 5);
    insert(root, 15);
    insert(root, 25);
    insert(root, 35);
    insert(root, 1);
    insert(root, 80);
    insert(root, 60);
    insert(root, 100);

    /* Get IN_ORDER traversal in the array */
    inorder(root, IN_ORDER);

    /* Get post order traversal in the array */
    postorder(root, POST_ORDER);

    CommonAncestor(1, 100);


}

0

Можливий ще один підхід. Однак він не такий ефективний, як той, що вже пропонується у відповідях.

  • Створіть вектор контуру для вузла n1.

  • Створіть другий вектор шляху для вузла n2.

  • Вектор шляху, що передбачає встановлені вузли від цього, повинен пройти, щоб досягти відповідного вузла.

  • Порівняйте обидва вектори шляху. Індекс, де вони не відповідають, повертають вузол у цьому індексі - 1. Це дало б LCA.

Мінуси такого підходу:

Для обчислення векторів шляху потрібно двічі обрізати дерево. Для зберігання векторів шляху потрібен додатковий O (h) простір.

Однак це легко здійснити і зрозуміти.

Код для обчислення вектора шляху:

private boolean findPathVector (TreeNode treeNode, int key, int pathVector[], int index) {

        if (treeNode == null) {
            return false;
        }

        pathVector [index++] = treeNode.getKey ();

        if (treeNode.getKey () == key) {
            return true;
        }
        if (findPathVector (treeNode.getLeftChild (), key, pathVector, index) || 
            findPathVector (treeNode.getRightChild(), key, pathVector, index)) {

            return true;        
        }

        pathVector [--index] = 0;
        return false;       
    }

0

Спробуйте так

node * lca(node * root, int v1,int v2)
{

if(!root) {
            return NULL;
    }
    if(root->data == v1 || root->data == v2) {
        return root;}
    else
    {
        if((v1 > root->data && v2 < root->data) || (v1 < root->data && v2 > root->data))
        {
            return root;
        }

        if(v1 < root->data && v2 < root->data)
        {
            root = lca(root->left, v1, v2);
        }

        if(v1 > root->data && v2 > root->data)
        {
            root = lca(root->right, v1, v2);
        }
    }
return root;
}

0

Сирий спосіб:

  • На кожному вузлі
    • X = знайти, якщо будь-який з n1, n2 існує з лівого боку Вузла
    • Y = знайти, якщо будь-який з n1, n2 існує праворуч від Вузла
      • якщо сам вузол n1 || n2, ми можемо назвати його або ліворуч, або праворуч для цілей узагальнення.
    • Якщо і X, і Y вірно, то Вузол є СА

Проблема з вищевказаним методом полягає в тому, що ми будемо робити "пошук" кілька разів, тобто є можливість кожного вузла пройти кілька разів. Ми можемо подолати цю проблему, якщо зможемо записати інформацію, щоб не обробляти її знову (подумайте про динамічне програмування).

Отже, замість того, щоб знаходити кожен вузол, ми ведемо облік того, що вже знайдено.

Кращий шлях:

  • Ми перевіряємо, чи є для даного вузла, якщо left_set (тобто n1 | n2 знайдено в лівому піддереві) або right_set спочатку глибиною. (ПРИМІТКА. Ми надаємо самому корінь властивість left_set, якщо це n1 | n2)
  • Якщо і лівий, і правий_сет, тоді вузол є LCA.

Код:

struct Node *
findCA(struct Node *root, struct Node *n1, struct Node *n2, int *set) {
   int left_set, right_set;
   left_set = right_set = 0;
   struct Node *leftCA, *rightCA;
   leftCA = rightCA = NULL;

   if (root == NULL) {
      return NULL;
   }
   if (root == n1 || root == n2) {
      left_set = 1;
      if (n1 == n2) {
         right_set = 1;
      }
   }

   if(!left_set) {
      leftCA = findCA(root->left, n1, n2, &left_set);
      if (leftCA) {
         return leftCA;
      }
   }
   if (!right_set) {
      rightCA= findCA(root->right, n1, n2, &right_set);
      if(rightCA) {
         return rightCA;
      }
   }

   if (left_set && right_set) {
      return root;
   } else {
      *set = (left_set || right_set);
      return NULL;
   }
}

0

Код для першого пошуку ширини, щоб переконатися, що обидва вузли на дереві. Тільки тоді рухайтеся вперед з пошуком LCA. Будь ласка, прокоментуйте, якщо у вас є якісь пропозиції щодо покращення. Я думаю, що ми, мабуть, можемо відзначити їх відвідуваними та відновити пошук у певний момент, коли ми припинили покращення для другого вузла (якщо його не знайдено ВІДОМО)

public class searchTree {
    static boolean v1=false,v2=false;
    public static boolean bfs(Treenode root, int value){
         if(root==null){
           return false;
     }
    Queue<Treenode> q1 = new LinkedList<Treenode>();

    q1.add(root);
    while(!q1.isEmpty())
    {
        Treenode temp = q1.peek();

        if(temp!=null) {
            q1.remove();
            if (temp.value == value) return true;
            if (temp.left != null) q1.add(temp.left);
            if (temp.right != null) q1.add(temp.right);
        }
    }
    return false;

}
public static Treenode lcaHelper(Treenode head, int x,int y){

    if(head==null){
        return null;
    }

    if(head.value == x || head.value ==y){
        if (head.value == y){
            v2 = true;
            return head;
        }
        else {
            v1 = true;
            return head;
        }
    }

    Treenode left = lcaHelper(head.left, x, y);
    Treenode right = lcaHelper(head.right,x,y);

    if(left!=null && right!=null){
        return head;
    }
    return (left!=null) ? left:right;
}

public static int lca(Treenode head, int h1, int h2) {
    v1 = bfs(head,h1);
    v2 = bfs(head,h2);
    if(v1 && v2){
        Treenode lca = lcaHelper(head,h1,h2);
        return lca.value;
    }
    return -1;
}
}

0

Ви вірні, що без батьківського вузла рішення з обходом дасть вам O (n) часову складність.

Підхід до обходу Припустимо, що ви знаходите LCA для вузлів A і B, найпростішим підходом є спочатку отримати шлях від кореня до A, а потім отримати шлях від кореня до B. Коли у вас є ці два шляхи, ви можете легко перебирати їх і знайти останній загальний вузол, який є найнижчим загальним предком A і B.

Рекурсивне рішення Іншим підходом є використання рекурсії. По-перше, ми можемо отримати LCA як з лівого дерева, так і з правого дерева (якщо існує). Якщо будь-який з A або B є кореневим вузлом, то коренем є LCA, і ми просто повертаємо корінь, який є кінцевою точкою рекурсії. Коли ми будемо ділити дерево на під дерева, врешті-решт, ми потрапимо або на A, і на B.

Щоб поєднати рішення підзадачі, якщо LCA (ліве дерево) повертає вузол, ми знаємо, що і A, і B розташовуються в лівому дереві, і повертається вузол є кінцевим результатом. Якщо і LCA (зліва), і LCA (праворуч) повертають непорожні вузли, це означає, що A і B знаходяться відповідно в лівому і правому дереві. У цьому випадку кореневий вузол є найнижчим загальним вузлом.

Перевірте найнижчого загального предка для детального аналізу та рішення.


0

Деякі рішення тут припускають, що існує посилання на кореневий вузол, деякі припускають, що дерево є BST. Спільне використання мого рішення за допомогою хешмапу, без посилання на rootвузол та дерево, може бути BST або non-BST:

    var leftParent : Node? = left
    var rightParent : Node? = right
    var map = [data : Node?]()

    while leftParent != nil {
        map[(leftParent?.data)!] = leftParent
        leftParent = leftParent?.parent
    }

    while rightParent != nil {
        if let common = map[(rightParent?.data)!] {
            return common
        }
        rightParent = rightParent?.parent
    }

0

Рішення 1: Рекурсивна - Швидше

  • Ідея полягає в тому, щоб обрізати дерево, починаючи від кореня. Якщо будь-який із заданих ключів p і q збігається з коренем, то корінь є LCA, припускаючи, що обидва ключі присутні. Якщо root не відповідає жодному з ключів, ми повторюємо для лівого та правого піддерева.
  • Вузол, який має один ключ, присутній у лівому піддереві, а інший ключ, присутній у правому піддіапараті, є LCA. Якщо обидві клавіші лежать у лівому піддереві, то і в лівому піддереві також є LCA, інакше LCA лежить у правому піддереві.
  • Складність часу: O (n)
  • Складність простору: O (h) - для рекурсивного стека викликів
class Solution 
{
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
    {
        if(root == null || root == p  || root == q)
            return root;

        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);

        if(left == null)
            return right;
        else if(right == null)
            return left;
        else
            return root;    // If(left != null && right != null)
    }
}

Рішення 2: Ітеративний - Використання батьківських покажчиків - Повільніше

  • Створіть порожню хеш-таблицю.
  • Вставте p та всіх його предків у хеш-таблицю.
  • Перевірте, чи існує q або будь-який з його предків у хеш-таблиці, якщо так, то поверніть першого наявного предка.
  • Складність часу: O (n) - У гіршому випадку ми можемо відвідати всі вузли двійкового дерева.
  • Складність простору: O (n) - простір, що використовує батьківський покажчик Хеш-таблиця, предка_набір і черга, буде O (n) кожен.
class Solution
{
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
    {
        HashMap<TreeNode, TreeNode> parent_map = new HashMap<>();
        HashSet<TreeNode> ancestors_set = new HashSet<>();
        Queue<TreeNode> queue = new LinkedList<>();

        parent_map.put(root, null);
        queue.add(root);

        while(!parent_map.containsKey(p) || !parent_map.containsKey(q))
        {
            TreeNode node = queue.poll();

            if(node.left != null)
            {
                parent_map.put(node.left, node);
                queue.add(node.left);
            }
            if(node.right != null)
            {
                parent_map.put(node.right, node);
                queue.add(node.right);
            }
        }

        while(p != null)
        {
            ancestors_set.add(p);
            p = parent_map.get(p);
        }

        while(!ancestors_set.contains(q))
            q = parent_map.get(q);

        return q;
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.