Come trovare l'antenato comune più basso di due nodi in qualsiasi albero binario?


187

L'albero binario qui potrebbe non essere necessariamente un albero di ricerca binario.
La struttura potrebbe essere presa come -

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

La soluzione massima che ho potuto elaborare con un amico era qualcosa del genere:
considera questo albero binario :

Albero binario

La traversata in ordine produce - 8, 4, 9, 2, 5, 1, 6, 3, 7

E l'attraversamento post-ordine produce: 8, 9, 4, 5, 2, 6, 7, 3, 1

Quindi, per esempio, se vogliamo trovare l'antenato comune dei nodi 8 e 5, allora facciamo un elenco di tutti i nodi che sono tra 8 e 5 nella traversata dell'albero in ordine, che in questo caso sembra essere [4, 9 , 2]. Quindi controlliamo quale nodo in questo elenco appare per ultimo nell'attraversamento postordine, che è 2. Quindi l'antenato comune per 8 e 5 è 2.

Credo che la complessità di questo algoritmo sia O (n) (O (n) per gli attraversamenti inorder / postorder, il resto dei passaggi è nuovamente O (n) poiché non sono altro che semplici iterazioni negli array). Ma c'è una forte possibilità che questo sia sbagliato. :-)

Ma questo è un approccio molto rozzo, e non sono sicuro che si rompa per qualche caso. Esiste un'altra soluzione (forse più ottimale) a questo problema?


6
Per curiosità, a che serve questo?
David Brunelle,

19
@David: la risposta alle query LCA è piuttosto utile. LCA + Suffix tree = potenti algoritmi relativi alle stringhe.

44
E quando ho posto una domanda simile, è stato votato con commenti come la sua domanda di intervista. Dualità di SO? :(
some_other_guy

5
@ Richiedente +1 per i dettagli forniti nella domanda. :)
amod

5
@DavidBrunelle Un'applicazione pratica per il calcolo dell'LCA: è un calcolo essenziale per il rendering di pagine Web, in particolare per il calcolo dei CSS (Cascading Style Sheets) applicabile a un particolare elemento DOM.
zc22

Risposte:


74

Nick Johnson ha ragione sul fatto che un algoritmo di complessità temporale O (n) è il migliore che puoi fare se non hai puntatori parent.) Per una versione ricorsiva semplice di tale algoritmo vedi il codice nel post di Kinding che gira in O (n) time .

Ma tieni presente che se i tuoi nodi hanno puntatori padre, è possibile un algoritmo migliorato. Per entrambi i nodi in questione costruire un elenco contenente il percorso dalla radice al nodo partendo dal nodo e inserendo frontalmente il genitore.

Quindi, per 8 nel tuo esempio, ottieni (mostrando i passaggi): {4}, {2, 4}, {1, 2, 4}

Fai lo stesso per l'altro nodo in questione, risultando (passaggi non mostrati): {1, 2}

Ora confronta i due elenchi che hai creato cercando il primo elemento in cui l'elenco differisce, o l'ultimo elemento di uno degli elenchi, a seconda di quale evento si verifica per primo.

Questo algoritmo richiede O (h) tempo in cui h è l'altezza dell'albero. Nel peggiore dei casi O (h) equivale a O (n), ma se l'albero è bilanciato, è solo O (log (n)). Richiede anche spazio O (h). È possibile una versione migliorata che utilizza solo spazio costante, con il codice mostrato nel post di CEGRD


Indipendentemente da come viene costruito l'albero, se questa sarà un'operazione che eseguirai molte volte sull'albero senza modificarlo nel mezzo, ci sono altri algoritmi che puoi usare che richiedono la preparazione del tempo O (n) [lineare], ma poi trova la coppia richiede solo O (1) tempo [costante]. Per riferimenti a questi algoritmi, vedere la pagina più bassa del problema relativo agli antenati comuni su Wikipedia . (Ringraziamo Jason per aver pubblicato questo link)


1
Questo fa il lavoro se viene fornito il puntatore genitore. I nodi nella struttura sono come la struttura che ho dato alla mia domanda: solo i puntatori figlio sinistro / destro, nessun puntatore principale. Esiste una soluzione O (log (n)) se non è disponibile alcun puntatore padre e l'albero non è un albero di ricerca binario ed è solo un albero binario?
Siddhant

2
Se non hai un modo particolare di trovare il percorso tra il genitore e un dato nodo, ci vorrà in media O (n) tempo per trovarlo. Ciò renderà impossibile avere il tempo O (log (n)). Tuttavia, il costo O (n) di una volta, con la ricerca della coppia O (1) potrebbe essere la soluzione migliore se avessi intenzione di eseguire questa operazione molte volte senza cambiare l'albero in mezzo. Altrimenti, se possibile, aggiungere il puntatore principale. Può rendere più veloci alcuni potenziali algoritmi, ma sono abbastanza sicuro che non cambi l'ordine di nessun algoritmo esistente. Spero che questo ti aiuti.
Kevin Cathcart,

1
questo approccio può essere fatto utilizzando O (1) Memoria - vedi soluzione Artelius di (e altri) a stackoverflow.com/questions/1594061/...
Tom Sirgedas

@ Tom: In effetti, ciò funzionerebbe per limitare la complessità della memoria a O (1) per l'algoritmo basato su elenco. Ovviamente ciò significa iterare attraverso l'albero stesso una volta una volta per ogni lato per ottenere la profondità dei nodi, quindi una seconda (parziale) seconda volta per trovare l'antenato comune. Il tempo O (h) e lo spazio O (1) sono chiaramente ottimali per il caso di avere puntatori padre e non eseguire precomputazione O (n).
Kevin Cathcart,

1
@ALBI O(h)è solo O(log(n))se l'albero è bilanciato. Per qualsiasi albero, che sia binario o meno, se hai puntatori padre puoi determinare il percorso da una foglia alla radice nel O(h)tempo, semplicemente seguendo il puntatore padre fino a hvolte. Questo ti dà il percorso dalla foglia alla radice. Se i percorsi sono memorizzati come una pila, iterando la pila si ottiene il percorso dalla radice alla foglia. Se mancano i puntatori padre e non si dispone di una struttura speciale per l'albero, la ricerca del percorso dalla radice alla foglia richiede O(n)tempo.
Kevin Cathcart,

108

A partire dal rootnodo e spostandosi verso il basso se trovi un nodo che ha uno poq come figlio diretto, allora è l'LCA. (modifica - questo dovrebbe essere se po qè il valore del nodo, restituirlo. Altrimenti fallirà quando uno po qè un figlio diretto dell'altro.)

Altrimenti se trovi un nodo con pnella sua sottostruttura destra (o sinistra) e qnella sua sottostruttura sinistra (o destra), allora è l'LCA.

Il codice fisso ha il seguente aspetto:

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

Il codice seguente non riesce quando uno dei due è figlio diretto dell'altro.

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

Codice in azione


2
soluzione elegante, ma root == p || root == q => restituisce il bit di root sembra troppo ottimistico. Cosa succede se si scopre che root è p / q, ma l'altro nodo ricercato non si trova effettivamente nella struttura?
Ian Durkan,

15
Immagino che questo codice fallisca quando p o q è un valore che non si trova nell'albero binario. Ho ragione? Ad esempio LCA (8,20). il codice ur restituisce 8. ma 20 non è presente nell'albero binario
javaMan

3
Qual è il costo per questa soluzione? È efficiente? Sembra continuare la ricerca anche dopo aver trovato sia p che q. È a causa della possibilità che p e q potrebbero non essere univoci nella struttura poiché non è un BST e possono contenere duplicati?
MikeB,

3
@MikeB, questa soluzione è sicuramente O (n), perché attraversi ogni nodo solo una volta nel peggiore dei casi. Peter Lee, questo è il modo più efficace per farlo senza usare i puntatori dei genitori. hai una soluzione migliore?
gsingh2011,

8
la prima soluzione imperfetta dovrebbe essere eliminata in modo da non distrarre
Zinan Xing

50

Ecco il codice di lavoro in 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
Questo non funziona quando non esiste un nodo nella struttura.
Pratik Khadloya,

ottimizzeresti il ​​tuo codice se l'albero dato fosse un BST?
Mona Jalal,

1
"Se la radice è una delle a o b, allora è la LCA." questo potrebbe non essere vero. Quello che sai a questo punto è che non è necessario controllare nessuno dei suoi figli per trovare l'LCA. Ciò accade perché in seguito possiamo verificare se per il genitore di root sono presenti corrispondenze su entrambi i rami (LCA è il genitore) o solo uno di essi (nel qual caso uno potrebbe essere l'LCA o un antenato ancora maggiore potrebbe essere l'LCA ).
Andrés

28

Le risposte fornite finora utilizzano la ricorsione o memorizzano, ad esempio, un percorso nella memoria.

Entrambi questi approcci potrebbero fallire se hai un albero molto profondo.

Ecco la mia opinione su questa domanda. Quando controlliamo la profondità (distanza dalla radice) di entrambi i nodi, se sono uguali, allora possiamo tranquillamente spostarci verso l'alto da entrambi i nodi verso l'antenato comune. Se una delle profondità è maggiore, dovremmo spostarci verso l'alto dal nodo più profondo rimanendo nell'altro.

Ecco il codice:

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;    

La complessità temporale di questo algoritmo è: O (n). La complessità spaziale di questo algoritmo è: O (1).

Per quanto riguarda il calcolo della profondità, possiamo prima ricordare la definizione: se v è root, depth (v) = 0; Altrimenti, depth (v) = depth (parent (v)) + 1. Possiamo calcolare la profondità come segue:

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

6
Gli alberi binari non hanno un riferimento all'elemento genitore, in genere. L'aggiunta di un riferimento principale può essere eseguita senza alcun problema, ma considererei lo spazio ausiliario O (n).
John Kurlak,

C'è un presupposto sottile in questa soluzione. Se un nodo è un genitore diretto o indiretto dell'altro (ovvero, il nodo più profondo si trova in un albero radicato nel nodo più superficiale), questa soluzione restituisce come risultato il genitore del nodo più superficiale. A seconda di come definisci l'antenato comune più basso, questo potrebbe non essere quello che desideri. Alcune definizioni richiedono che il nodo meno profondo stesso sia il genitore. In questo caso, dovresti tenere traccia di quale sia il nodo più superficiale e restituirlo.
Srikanth,

8

Bene, questo tipo di dipende da come è strutturato il tuo albero binario. Presumibilmente hai un modo per trovare il nodo foglia desiderato dato la radice dell'albero - semplicemente applicalo a entrambi i valori fino a quando i rami che hai scelto divergono.

Se non hai un modo per trovare la foglia desiderata data la radice, la tua unica soluzione - sia in funzionamento normale che per trovare l'ultimo nodo comune - è una ricerca della forza bruta dell'albero.


8

Questo può essere trovato su: - 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;
        }
    }
}

puoi dirmi come si comporterà il tuo codice se p è presente ma q non è affatto presente nella struttura? Allo stesso modo sia p che q non sono presenti. Grazie!!!
Prova il

Qual è la grande O in termini di tempo? Penso che sia O (n * log (n)), due lenti.
Peter Lee


6

Per scoprire l'antenato comune di due nodi: -

  • Trova il nodo Nodo1 specificato nella struttura utilizzando la ricerca binaria e salva tutti i nodi visitati in questo processo in un array come A1. Time - O (logn), Space - O (logn)
  • Trova il nodo 2 specificato nella struttura utilizzando la ricerca binaria e salva tutti i nodi visitati in questo processo in un array, ad esempio A2. Time - O (logn), Space - O (logn)
  • Se l'elenco A1 o l'elenco A2 è vuoto, allora il nodo non esiste quindi non esiste un antenato comune.
  • Se l'elenco A1 e l'elenco A2 non sono vuoti, cerca nell'elenco fino a quando non trovi un nodo non corrispondente. Non appena si trova un nodo del genere, il nodo precedente è un antenato comune.

Funzionerebbe con l'albero di ricerca binario.


2
Ha chiaramente affermato che l'albero NON è necessariamente un BST.
Peter Lee,

@Peter Lee - La logica di cui sopra funzionerebbe anche per qualsiasi albero binario con una semplice modifica. Al posto della ricerca binaria di determinati nodi, applica la ricerca lineare (ovvero qualsiasi attraversamento, ma dovrebbe essere la stessa per entrambi i casi). Il runtime fuori rotta sarebbe O (n) invece di O (logn). In realtà questo algo è il più robusto quando il puntatore parent non è disponibile. L'algoritmo rucursivo fornito da molti (vale a dire 'codaddict') non funzionerà quando uno di un dato nodo non appartiene all'albero)
KGhatak


3

L'algoritmo ricorsivo sottostante verrà eseguito in O (log N) per un albero binario bilanciato. Se uno dei nodi passati nella funzione getLCA () sono gli stessi della radice, allora la radice sarà l'LCA e non sarà necessario eseguire alcuna recussione.

Casi test. [1] Entrambi i nodi n1 e n2 sono nella struttura e risiedono su entrambi i lati del nodo padre. [2] Il nodo n1 o n2 è il root, l'LCA è il root. [3] Nell'albero è presente solo n1 o n2, l'LCA sarà il nodo radice della sottostruttura sinistra della radice dell'albero, oppure l'LCA sarà il nodo radice della sottostruttura destra dell'albero radice.

[4] N1 o n2 non sono presenti nell'albero, non esiste alcun LCA. [5] Sia n1 che n2 sono in linea retta l'uno accanto all'altro, LCA sarà di n1 o n2 che mai si chiude alla radice dell'albero.

//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

Basta scendere da tutto l'albero rootfintanto che entrambi i nodi dati, diciamo peq , per i quali si deve trovare Ancestor, sono nello stesso sotto-albero (il che significa che i loro valori sono entrambi più piccoli o entrambi più grandi di quelli di root).

Questo cammina direttamente dalla radice all'antenato meno comune, senza guardare il resto dell'albero, quindi è praticamente più veloce che mai. Alcuni modi per farlo.

Iterativo, O (1) spazio

Pitone

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

Giava

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

in caso di overflow, farei (root.val - (long) p.val) * (root.val - (long) q.val)

Ricorsivo

Pitone

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

Giava

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

Considera questo albero inserisci qui la descrizione dell'immagine

Se eseguiamo il postorder e il preordine attraversiamo e troviamo il primo predecessore e successore comune occorrente, otteniamo l'antenato comune.

postordine => 0,2,1,5,4,6,3,8,10,11,9,14,15,13,12,7 preordine => 7,3,1,0,2,6,4 , 5,12,9,8,11,10,13,15,14

  • ad es .: 1

Meno antenato comune di 8,11

nel postordine abbiamo => 9,14,15,13,12,7 dopo 8 e 11 in preordine abbiamo => 7,3,1,0,2,6,4,5,12,9 prima di 8 e 11

9 è il primo numero comune che si verifica dopo 8 e 11 in postordine e prima di 8 e 11 in preordine, quindi 9 è la risposta

  • es: 2

Meno antenato comune di 5,10

11,9,14,15,13,12,7 in ordine post 7,3,1,0,2,6,4 in ordine

7 è il primo numero che si verifica dopo 5,10 in ordine e prima di 5,10 in ordine, quindi 7 è la risposta


2

Se è un albero binario completo con figli del nodo x come 2 * xe 2 * x + 1 di quanto non ci sia un modo più veloce per farlo

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

Come funziona

  1. ottenere i bit necessari per rappresentare x & y che utilizzando la ricerca binaria è O (log (32))
  2. il prefisso comune della notazione binaria di x & y è l'antenato comune
  3. qualunque sia rappresentato da un numero maggiore di bit viene portato allo stesso bit da k >> diff
  4. k = x ^ y cancella il prefisso comune di x & y
  5. trova bit che rappresentano il suffisso rimanente
  6. sposta x o y per bit di suffisso per ottenere il prefisso comune che è l'antenato comune.

Questo funziona perché fondamentalmente dividi il numero più grande per due in modo ricorsivo fino a quando entrambi i numeri sono uguali. Quel numero è l'antenato comune. La divisione è effettivamente la giusta opzione di spostamento. Quindi dobbiamo trovare un prefisso comune di due numeri per trovare l'antenato più vicino


2

In scala puoi:

  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

Ecco il modo C ++ per farlo. Ho cercato di mantenere l'algoritmo il più semplice possibile da capire:

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

Come usarlo:

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

0

Il modo più semplice per trovare l'antenato più basso è utilizzare il seguente algoritmo:

Esamina il nodo principale

se value1 e value2 sono strettamente inferiori al valore nel nodo radice 
    Esamina la sottostruttura sinistra
altrimenti se value1 e value2 sono strettamente maggiori del valore nel nodo radice 
    Esamina la sottostruttura corretta
altro
    radice di ritorno
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
NON è un BST!
Peter Lee,

0

Ho trovato una soluzione

  1. Prendi l'ordine
  2. Prendi il preordine
  3. Prendi l'ordine

A seconda di 3 attraversamenti, puoi decidere chi è l'LCA. Da LCA trovare la distanza di entrambi i nodi. Aggiungi queste due distanze, che è la risposta.


0

Ecco cosa penso

  1. Trova il percorso per il nodo del pugno, memorizzalo su arr1.
  2. Inizia a trovare la route per il nodo 2, mentre fai così controlla ogni valore da root a arr1.
  3. tempo in cui il valore differisce, esci. Il vecchio valore corrispondente è l'LCA.

Complessità: passaggio 1: O (n), passaggio 2 = ~ O (n), totale = ~ O (n).


0

Ecco due approcci in c # (.net) (entrambi discussi sopra) come riferimento:

  1. Versione ricorsiva di ricerca di LCA nell'albero binario (O (N) - come al massimo ogni nodo è visitato) (i punti principali della soluzione è LCA è (a) l' unico nodo nell'albero binario in cui entrambi gli elementi risiedono su entrambi i lati dei sottotitoli (a sinistra e a destra) è LCA. (b) E anche non importa quale nodo sia presente su entrambi i lati - inizialmente ho cercato di mantenere quelle informazioni, e ovviamente la funzione ricorsiva diventa così confusa. Una volta che me ne sono reso conto, è diventato molto elegante.

  2. Cercare entrambi i nodi (O (N)) e tenere traccia dei percorsi (usa spazio extra - quindi, # 1 è probabilmente superiore anche se lo spazio è probabilmente trascurabile se l'albero binario è ben bilanciato in quanto il consumo di memoria extra sarà solo in O (log (N)).

    in modo che i percorsi vengano confrontati (sostanzialmente simile alla risposta accettata, ma i percorsi vengono calcolati ipotizzando che il nodo puntatore non sia presente nel nodo dell'albero binario)

  3. Solo per il completamento ( non correlato alla domanda ), LCA in BST (O (log (N))

  4. test

Ricorsivo:

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

dove sopra viene invocata la versione privata ricorsiva con il seguente metodo pubblico:

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

Soluzione tenendo traccia dei percorsi di entrambi i nodi:

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

dove FindNodeAndPath è definito come

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) - non correlato (solo per completamento per riferimento)

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

Test unitari

[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

Se qualcuno è interessato allo pseudo codice (per i lavori a casa dell'università) eccone uno.

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

Sebbene sia già stata data una risposta, questo è il mio approccio a questo problema usando il linguaggio di programmazione C. Sebbene il codice mostri un albero di ricerca binario (per quanto riguarda insert ()), ma l'algoritmo funziona anche per un albero binario. L'idea è di passare su tutti i nodi che si trovano dal nodo A al nodo B in traversal inorder, cercare gli indici per questi nel traversal post order. Il nodo con indice massimo nell'attraversamento dell'ordine postale è l'antenato comune più basso.

Questo è un codice C funzionante per implementare una funzione per trovare l'antenato comune più basso in un albero binario. Sto fornendo anche tutte le funzioni di utilità ecc., Ma vai a CommonAncestor () per una rapida comprensione.

#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

Può esserci un altro approccio. Tuttavia non è efficiente come quello già suggerito nelle risposte.

  • Creare un vettore di percorso per il nodo n1.

  • Creare un secondo vettore di percorso per il nodo n2.

  • Il vettore di percorso che implica i nodi impostati da quello attraverserebbe per raggiungere il nodo in questione.

  • Confronta entrambi i vettori di percorso. L'indice in cui non corrispondono, restituisce il nodo in quell'indice - 1. Questo darebbe l'LCA.

Contro per questo approccio:

È necessario attraversare l'albero due volte per calcolare i vettori del percorso. È necessario spazio O (h) aggiuntivo per memorizzare i vettori del percorso.

Tuttavia, anche questo è facile da implementare e da capire.

Codice per il calcolo del vettore di percorso:

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

Prova così

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

Modo grezzo:

  • Ad ogni nodo
    • X = trova se uno dei n1, n2 esiste sul lato sinistro del nodo
    • Y = trova se uno dei n1, n2 esiste sul lato destro del Nodo
      • se il nodo stesso è n1 || n2, possiamo chiamarlo trovato a sinistra oa destra ai fini della generalizzazione.
    • Se sia X che Y sono veri, allora il nodo è la CA.

Il problema con il metodo sopra è che faremo il "find" più volte, cioè c'è la possibilità che ogni nodo venga attraversato più volte. Possiamo superare questo problema se possiamo registrare le informazioni in modo da non elaborarle di nuovo (pensate alla programmazione dinamica).

Quindi, piuttosto che trovare ogni nodo, teniamo un registro di ciò che è già stato trovato.

Modo migliore:

  • Controlliamo per vedere se per un dato nodo se left_set (ovvero n1 | n2 è stato trovato nella sottostruttura sinistra) o right_set in un primo modo approfondito. (NOTA: stiamo dando alla radice stessa la proprietà di essere left_set se è n1 | n2)
  • Se sia left_set che right_set il nodo è un LCA.

Codice:

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

Code for A Breadth First Cerca per assicurarti che entrambi i nodi siano nella struttura. Solo allora vai avanti con la ricerca LCA. Si prega di commentare se avete suggerimenti per migliorare. Penso che probabilmente possiamo contrassegnarli come visitati e riavviare la ricerca in un certo punto in cui abbiamo interrotto per migliorare il secondo nodo (se non viene trovato VISITATO)

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

Hai ragione a dire che senza un nodo genitore, la soluzione con attraversamento ti darà O (n) complessità temporale.

Approccio trasversale Supponiamo che tu stia trovando LCA per i nodi A e B, l'approccio più semplice è quello di ottenere prima il percorso dalla radice ad A e quindi ottenere il percorso dalla radice a B. Una volta che hai questi due percorsi, puoi facilmente iterarli e trova l'ultimo nodo comune, che è l'antenato comune più basso di A e B.

Soluzione ricorsiva Un altro approccio è utilizzare la ricorsione. In primo luogo, possiamo ottenere LCA sia dall'albero sinistro che dall'albero destro (se esiste). Se uno di A o B è il nodo radice, allora la radice è l'LCA e restituiamo semplicemente la radice, che è il punto finale della ricorsione. Mentre continuiamo a dividere l'albero in sotto-alberi, alla fine, colpiremo A e B.

Per combinare soluzioni con problemi secondari, se LCA (albero a sinistra) restituisce un nodo, sappiamo che sia A che B individuano nell'albero a sinistra e il nodo restituito è il risultato finale. Se sia LCA (sinistra) che LCA (destra) restituiscono nodi non vuoti, significa che A e B sono rispettivamente nella struttura sinistra e destra. In questo caso, il nodo principale è il nodo comune più basso.

Controlla l' antenato comune più basso per analisi e soluzioni dettagliate.


0

Alcune delle soluzioni qui presuppongono che ci sia un riferimento al nodo radice, alcune ipotizzano che l'albero sia un BST. Condividere la mia soluzione usando hashmap, senza riferimento a rootnodo e albero può essere BST o 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

Soluzione 1: ricorsivo: più veloce

  • L'idea è di attraversare l'albero partendo dalla radice. Se una qualsiasi delle chiavi date peq corrisponde a root, allora root è LCA, supponendo che entrambe le chiavi siano presenti. Se root non corrisponde a nessuna delle chiavi, facciamo ricorso alla sottostruttura sinistra e destra.
  • Il nodo che ha una chiave presente nella sua sottostruttura sinistra e l'altra chiave presente nella sottostruttura destra è l'LCA. Se entrambe le chiavi si trovano nella sottostruttura sinistra, anche la sottostruttura sinistra ha LCA, altrimenti LCA si trova nella sottostruttura destra.
  • Complessità temporale: O (n)
  • Complessità spaziale: O (h) - per stack di chiamate ricorsivo
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)
    }
}

Soluzione 2: Iterativo - Utilizzo dei puntatori padre - Più lento

  • Crea una tabella hash vuota.
  • Inserisci p e tutti i suoi antenati nella tabella hash.
  • Controlla se q o uno dei suoi antenati esiste nella tabella hash, in caso affermativo restituisci il primo antenato esistente.
  • Complessità temporale: O (n) - Nel peggiore dei casi potremmo visitare tutti i nodi dell'albero binario.
  • Complessità spaziale: O (n) - Lo spazio utilizzato dal puntatore padre Hash-table, ancestor_set e coda, sarebbe O (n) ciascuno.
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;
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.