190 votes

Comment trouver l'ancêtre commun le plus bas de deux nœuds dans un arbre binaire ?

L'arbre binaire n'est pas nécessairement un arbre de recherche binaire.
La structure peut être considérée comme -

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

La solution maximale que j'ai pu trouver avec un ami était de l'ordre de -
Considérer cet arbre binaire :

Binary Tree

Le parcours dans l'ordre donne - 8, 4, 9, 2, 5, 1, 6, 3, 7

Et la traversée post-ordre donne - 8, 9, 4, 5, 2, 6, 7, 3, 1

Ainsi, par exemple, si nous voulons trouver l'ancêtre commun des nœuds 8 et 5, nous dressons une liste de tous les nœuds situés entre 8 et 5 dans l'ordre de parcours de l'arbre, qui, dans ce cas, est [4, 9, 2]. Ensuite, nous vérifions quel nœud de cette liste apparaît en dernier dans le parcours post-ordre, qui est 2. Par conséquent, l'ancêtre commun de 8 et 5 est 2.

La complexité de cet algorithme est, je crois, de O(n) (O(n) pour les traversées dans l'ordre/post-ordre, le reste des étapes étant à nouveau de O(n) puisqu'il ne s'agit que de simples itérations dans des tableaux). Mais il y a de fortes chances que ce soit faux :-)

Mais il s'agit d'une approche très rudimentaire, et je ne suis pas sûr qu'elle ne fonctionne pas dans certains cas. Existe-t-il une autre solution (éventuellement plus optimale) à ce problème ?

6 votes

Par curiosité, quelle en est l'utilité pratique ?

19 votes

@David : La réponse aux requêtes LCA est très utile. LCA + arbre des suffixes = algorithmes puissants liés aux chaînes de caractères.

44 votes

Et lorsque j'ai posé une question similaire, elle a été rejetée avec des commentaires comme la question de l'entretien. Dualité du SO ? :(

109voto

codaddict Points 154968

À partir de root et en se déplaçant vers le bas si l'on trouve un nœud qui possède l'une ou l'autre des caractéristiques suivantes p o q comme son enfant direct, il s'agit alors de l'ACL. (edit - ceci devrait être si p o q est la valeur du nœud, elle est renvoyée. Sinon, il échouera si l'un des éléments suivants p o q est un enfant direct de l'autre).

Sinon, si vous trouvez un nœud avec p dans son sous-arbre de droite (ou de gauche) et q dans son sous-arbre gauche (ou droit), il s'agit alors de l'ACL.

Le code fixé se présente comme suit :

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

Le code ci-dessous échoue si l'un des deux est l'enfant direct de l'autre.

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

Le code en action

3 votes

Solution élégante, mais l'option Racine==p || Racine==q => retour Racine semble trop optimiste. Et s'il s'avère que Racine est p/q, mais que l'autre noeud recherché n'est pas dans l'arbre ?

0 votes

@codaddict Quelle sera la complexité temporelle de cette solution ?

16 votes

Je suppose que ce code échoue lorsque p ou q est une valeur qui n'est pas dans l'arbre binaire. N'ai-je pas raison ? Par exemple LCA(8,20). Notre code renvoie 8, mais 20 n'est pas présent dans l'arbre binaire.

74voto

Kevin Cathcart Points 3521

Nick Johnson a raison de dire qu'un algorithme de complexité temporelle O(n) est le meilleur que l'on puisse faire si l'on n'a pas de pointeurs parents). Pour une version récursive simple de cet algorithme, voir le code dans Poste de Kinding qui s'exécute en O(n) temps.

Mais gardez à l'esprit que si vos nœuds ont des pointeurs de parent, un algorithme amélioré est possible. Pour les deux nœuds en question, construisez une liste contenant le chemin de la racine au nœud en commençant par le nœud et en insérant le parent à l'avant.

Ainsi, pour 8 dans votre exemple, vous obtenez (en montrant les étapes) : {4}, {2, 4}, {1, 2, 4}

Faites de même pour l'autre nœud en question, ce qui donne (étapes non indiquées) : {1, 2}

Comparez maintenant les deux listes que vous avez créées en recherchant le premier élément où les listes diffèrent, ou le dernier élément de l'une des listes, selon ce qui arrive en premier.

Cet algorithme nécessite un temps O(h) où h est la hauteur de l'arbre. Dans le pire des cas, O(h) équivaut à O(n), mais si l'arbre est équilibré, ce temps n'est que O(log(n)). Il nécessite également O(h) d'espace. Il existe une version améliorée qui n'utilise qu'un espace constant, dont le code est présenté dans le document Poste du CEGRD


Indépendamment de la manière dont l'arbre est construit, si cette opération doit être effectuée plusieurs fois sur l'arbre sans le modifier entre-temps, vous pouvez utiliser d'autres algorithmes qui nécessitent une préparation en temps O(n) [linéaire], mais la recherche d'une paire ne prend que O(1) [constant]. Pour des références à ces algorithmes, voir la page du problème du plus petit ancêtre commun sur Wikipedia (en anglais) . (Crédit à Jason pour avoir posté ce lien à l'origine)

1 votes

Cela fait l'affaire si le pointeur parent est donné. Les nœuds de l'arbre correspondent à la structure que j'ai donnée dans ma question - juste les pointeurs d'enfants gauche/droite, pas de pointeur de parent. Existe-t-il une solution O(log(n)) si aucun pointeur de parent n'est disponible et si l'arbre n'est pas un arbre de recherche binaire, mais simplement un arbre binaire ?

2 votes

Si vous n'avez pas de méthode particulière pour trouver le chemin entre le parent et un nœud donné, il faudra en moyenne O(n) pour le trouver. Il est donc impossible d'obtenir un temps O(log(n)). Cependant, le coût unique de O(n), avec O(1) pour la recherche de paires, pourrait être votre meilleur choix si vous deviez effectuer cette opération plusieurs fois sans changer l'arbre entre-temps. Dans le cas contraire, vous devriez, si possible, ajouter le pointeur de parent. Il peut rendre plusieurs algorithmes potentiels plus rapides, mais je suis presque sûr qu'il ne modifie pas l'ordre d'un algorithme existant. J'espère que cela vous aidera.

1 votes

Cette approche peut être réalisée en utilisant une mémoire O(1) -- voir la solution d'Artelius (et d'autres) à l'adresse suivante stackoverflow.com/questions/1594061/

51voto

Akash Verma Points 207

Voici le code de travail en 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 votes

Cela ne fonctionne pas lorsqu'un nœud n'existe pas dans l'arbre.

0 votes

Optimiserez-vous votre code si l'arbre donné est un BST ?

1 votes

"Si la racine est a ou b, il s'agit de l'ACL", ce n'est peut-être pas vrai. Ce que vous savez à ce stade, c'est qu'il n'est pas nécessaire de vérifier l'un de ses enfants pour trouver l'ACL. En effet, nous pouvons vérifier ultérieurement si, pour le parent de la racine, il existe des correspondances sur les deux branches (l'ACL est le parent) ou seulement sur l'une d'entre elles (dans ce cas, il peut s'agir de l'ACL, ou d'un ancêtre encore plus grand qui peut être l'ACL).

30voto

CEGRD Points 2233

Les réponses données jusqu'à présent utilisent la récursivité ou stockent, par exemple, un chemin en mémoire.

Ces deux approches peuvent échouer si l'arbre est très profond.

Voici mon point de vue sur cette question. Lorsque nous vérifions la profondeur (distance par rapport à la racine) des deux nœuds, si elle est égale, nous pouvons en toute sécurité remonter des deux nœuds vers l'ancêtre commun. Si l'un des nœuds est plus profond, nous devons nous déplacer vers le haut à partir du nœud le plus profond tout en restant dans l'autre nœud.

Voici le code :

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 complexité temporelle de cet algorithme est la suivante O(n). La complexité en espace de cet algorithme est : O(1).

En ce qui concerne le calcul de la profondeur, nous pouvons d'abord rappeler la définition : Si v est Racine, profondeur(v) = 0 ; Sinon, profondeur(v) = profondeur(parent(v)) + 1. Nous pouvons calculer la profondeur comme suit :

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

6 votes

Les arbres binaires n'ont généralement pas de référence à l'élément parent. L'ajout d'une référence au parent peut se faire sans problème, mais je considérerais qu'il s'agit d'un espace auxiliaire O(n).

0 votes

Cette solution repose sur une hypothèse subtile. Si un noeud est un parent direct ou indirect de l'autre (c'est-à-dire que le noeud le plus profond est dans un arbre enraciné au noeud le moins profond), cette solution renvoie le parent du noeud le moins profond comme résultat. Selon la façon dont vous définissez le plus petit ancêtre commun, il se peut que ce ne soit pas ce que vous voulez. Certaines définitions exigent que le nœud le moins profond soit lui-même le parent. Dans ce cas, vous devrez déterminer quel est le nœud le moins profond et le retourner.

9voto

Nick Johnson Points 79909

Cela dépend de la structure de votre arbre binaire. Vous avez probablement un moyen de trouver le nœud feuille désiré en fonction de la racine de l'arbre - appliquez simplement cette méthode aux deux valeurs jusqu'à ce que les branches que vous choisissez divergent.

Si vous ne disposez pas d'un moyen de trouver la feuille souhaitée à partir de la racine, votre seule solution - tant pour le fonctionnement normal que pour trouver le dernier nœud commun - consiste à effectuer une recherche brute dans l'arbre.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X