L'arbre binaire ici n'est pas nécessairement un arbre de recherche binaire.
La structure pourrait être considérée comme -
struct node {
int data;
struct node *left;
struct node *right;
};
La solution maximale que je pourrais trouver avec un ami était quelque chose de ce genre -
Considérez cet arbre binaire :
La traversée en 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, alors nous faisons une liste de tous les nœuds qui sont entre 8 et 5 dans le parcours d'arbre inorder, qui dans ce cas se trouve être [4, 9 , 2]. Ensuite, nous vérifions quel nœud de cette liste apparaît en dernier dans le parcours de post-ordre, qui est 2. Par conséquent, l'ancêtre commun pour 8 et 5 est 2.
La complexité de cet algorithme, je crois, est O (n) (O (n) pour les traversées en ordre / post-ordre, le reste des étapes étant à nouveau O (n) car ce ne sont rien de plus que de simples itérations dans des tableaux). Mais il y a de fortes chances que ce soit faux. :-)
Mais c'est une approche très grossière, et je ne suis pas sûr si cela échoue dans certains cas. Existe-t-il une autre solution (peut-être plus optimale) à ce problème?
Réponses:
Nick Johnson a raison de dire qu'un algorithme de complexité en temps O (n) est le meilleur que vous puissiez faire si vous n'avez pas de pointeurs parents.) Pour une version récursive simple de cet algorithme, voir le code dans l'article de Kinding qui s'exécute en temps O (n) .
Mais gardez à l'esprit que si vos nœuds ont des pointeurs parents, 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 au nœud, puis en insérant le parent.
Donc, pour 8 dans votre exemple, vous obtenez (montrant les étapes): {4}, {2, 4}, {1, 2, 4}
Faites de même pour votre autre nœud en question, ce qui entraîne (étapes non illustrées): {1, 2}
Maintenant, comparez les deux listes que vous avez faites en recherchant le premier élément où la liste diffère, ou le dernier élément d'une des listes, selon la première éventualité.
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é, c'est seulement O (log (n)). Il nécessite également un espace O (h). Une version améliorée est possible qui n'utilise que l'espace constant, avec le code affiché dans l' article du CEGRD
Indépendamment de la façon dont l'arbre est construit, s'il s'agit d'une opération que vous effectuez plusieurs fois sur l'arbre sans le changer entre les deux, il existe d'autres algorithmes que vous pouvez utiliser qui nécessitent une préparation de temps O (n) [linéaire], mais en trouvant ensuite paire ne prend que le temps O (1) [constant]. Pour obtenir des références à ces algorithmes, consultez la page sur les problèmes d'ancêtres communs les plus bas sur Wikipedia . (Merci à Jason d'avoir publié ce lien à l'origine)
la source
O(h)
n'est queO(log(n))
si l'arbre est équilibré. Pour n'importe quel arbre, qu'il soit binaire ou non, si vous avez des pointeurs parents, vous pouvez déterminer le chemin d'une feuille à la racine dans leO(h)
temps, simplement en suivant le pointeur parent jusqu'à desh
heures. Cela vous donne le chemin de la feuille à la racine. Si les chemins sont stockés sous forme de pile, l'itération de la pile vous donne le chemin de la racine à la feuille. Si vous n'avez pas de pointeurs parents et que vous n'avez pas de structure spéciale pour l'arborescence, trouver le chemin de la racine à la feuille prend duO(n)
temps.En partant du
root
nœud et en descendant si vous trouvez un nœud qui a l'unp
ou l' autreq
comme enfant direct, c'est l'ACV. (modifier - cela devrait être sip
ouq
est la valeur du nœud, renvoyez-le. Sinon, cela échouera lorsque l'un dep
ouq
est un enfant direct de l'autre.)Sinon, si vous trouvez un nœud avec
p
dans son sous-arbre droit (ou gauche) etq
dans son sous-arbre gauche (ou droit), il s'agit de l'ACV.Le code fixe ressemble à:
Le code ci-dessous échoue lorsque l'un est l'enfant direct de l'autre.
Code en action
la source
Voici le code de travail en JAVA
la source
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 vous avez un arbre très profond.
Voici mon point de vue sur cette question. Lorsque nous vérifions la profondeur (distance de la racine) des deux nœuds, s'ils sont égaux, nous pouvons alors remonter en toute sécurité des deux nœuds vers l'ancêtre commun. Si l'une des profondeurs est plus grande, nous devrions remonter du nœud le plus profond tout en restant dans l'autre.
Voici le code:
La complexité temporelle de cet algorithme est: O (n). La complexité spatiale de cet algorithme est: O (1).
En ce qui concerne le calcul de la profondeur, nous pouvons d'abord nous souvenir de la définition: Si v est racine, profondeur (v) = 0; Sinon, profondeur (v) = profondeur (parent (v)) + 1. Nous pouvons calculer la profondeur comme suit:
la source
Eh bien, cela dépend de la structure de votre arbre binaire. Vraisemblablement, vous avez un moyen de trouver le nœud feuille souhaité en fonction de la racine de l'arbre - appliquez-le simplement 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, alors votre seule solution - à la fois en fonctionnement normal et pour trouver le dernier nœud commun - est une recherche par force brute de l'arbre.
la source
Cela peut être trouvé à: - http://goursaha.freeoda.com/DataStructure/LowestCommonAncestor.html
la source
L'algorithme des ancêtres les moins communs hors ligne de Tarjan est assez bon (cf. aussi Wikipedia ). Il y a plus sur le problème (le plus petit problème d'ancêtre commun) sur Wikipedia .
la source
Pour découvrir l'ancêtre commun de deux nœuds: -
Cela fonctionnerait pour l'arbre de recherche binaire.
la source
J'ai fait une tentative avec des images illustratives et du code de travail en Java,
http://tech.bragboy.com/2010/02/least-common-ancestor-without-using.html
la source
L'algorithme récursif ci-dessous fonctionnera en O (log N) pour un arbre binaire équilibré. Si l'un des nœuds passés dans la fonction getLCA () est le même que la racine, la racine sera l'ACV et il ne sera pas nécessaire d'effectuer une recussrion.
Cas de test. [1] Les deux nœuds n1 et n2 sont dans l'arborescence et résident de chaque côté de leur nœud parent. [2] Le nœud n1 ou n2 est la racine, le LCA est la racine. [3] Seul n1 ou n2 est dans l'arborescence, LCA sera soit le nœud racine du sous-arbre gauche de la racine de l'arbre, soit l'ACV sera le nœud racine du sous-arbre droit de la racine de l'arbre.
[4] Ni n1 ni n2 ne sont dans l'arborescence, il n'y a pas de LCA. [5] Les deux n1 et n2 sont en ligne droite l'un à côté de l'autre, LCA sera soit de n1 ou n2 qui se rapproche jamais de la racine de l'arbre.
la source
Il suffit de descendre de l'arbre entier
root
tant que les deux nœuds donnés, disonsp
etq
, pour lesquels l'ancêtre doit être trouvé, sont dans le même sous-arbre (ce qui signifie que leurs valeurs sont à la fois plus petites ou plus grandes que celles de racine).Cela va directement de la racine à l'ancêtre le moins commun, sans regarder le reste de l'arbre, donc c'est à peu près aussi rapide que possible. Quelques façons de le faire.
en cas de débordement, je ferais (root.val - (long) p.val) * (root.val - (long) q.val)
la source
la source
Considérez cet arbre
Si nous effectuons une traversée de post-ordre et de pré-ordre et trouvons le premier prédécesseur et successeur commun, nous obtenons l'ancêtre commun.
postorder => 0,2,1,5,4,6,3,8,10,11,9,14,15,13,12,7 preorder => 7,3,1,0,2,6,4 , 5,12,9,8,11,10,13,15,14
Ancêtre le moins commun de 8,11
en postorder nous avons => 9,14,15,13,12,7 après 8 & 11 en précommande nous avons => 7,3,1,0,2,6,4,5,12,9 avant 8 & 11
9 est le premier nombre courant qui apparaît après 8 et 11 en post-commande et avant 8 et 11 en précommande, donc 9 est la réponse
Ancêtre le moins commun de 5,10
11,9,14,15,13,12,7 en post-commande 7,3,1,0,2,6,4 en précommande
7 est le premier nombre qui apparaît après 5,10 en post-commande et avant 5,10 en précommande, donc 7 est la réponse
la source
S'il s'agit d'un arbre binaire complet avec des enfants du nœud x comme 2 * x et 2 * x + 1, il existe un moyen plus rapide de le faire
Comment ça marche
Cela fonctionne parce que fondamentalement, divisez le plus grand nombre par deux récursivement jusqu'à ce que les deux nombres soient égaux. Ce nombre est l'ancêtre commun. La division est effectivement la bonne opération de changement de vitesse. Nous devons donc trouver le préfixe commun de deux nombres pour trouver l'ancêtre le plus proche
la source
Dans scala, vous pouvez:
la source
la source
Voici la manière C ++ de le faire. J'ai essayé de garder l'algorithme aussi simple que possible à comprendre:
Comment l'utiliser:
la source
Le moyen le plus simple de trouver l'ancêtre commun le plus bas consiste à utiliser l'algorithme suivant:
la source
J'ai trouvé une solution
En fonction de 3 parcours, vous pouvez décider qui est le LCA. À partir de LCA, trouvez la distance des deux nœuds. Ajoutez ces deux distances, qui est la réponse.
la source
Voici ce que je pense,
Complexité: étape 1: O (n), étape 2 = ~ O (n), total = ~ O (n).
la source
Voici deux approches en c # (.net) (toutes deux discutées ci-dessus) pour référence:
Version récursive de la recherche de LCA dans l'arbre binaire (O (N) - car au plus chaque nœud est visité) (les principaux points de la solution est LCA est (a) le seul nœud de l'arbre binaire où les deux éléments résident de chaque côté des sous-arbres (à gauche et à droite) est LCA. (b) Et aussi peu importe quel nœud est présent de chaque côté - au départ j'ai essayé de garder cette information, et évidemment la fonction récursive est devenue si déroutante.Une fois que je l'ai réalisée, elle est devenue très élégante.
La recherche des deux nœuds (O (N)) et le suivi des chemins (utilise de l'espace supplémentaire - donc, # 1 est probablement supérieur même si l'espace est probablement négligeable si l'arbre binaire est bien équilibré, car la consommation de mémoire supplémentaire sera juste en O (log (N)).
de sorte que les chemins sont comparés (essentiellement similaire à la réponse acceptée - mais les chemins sont calculés en supposant que le nœud de pointeur n'est pas présent dans le nœud de l'arbre binaire)
Juste pour compléter ( sans rapport avec la question ), LCA en BST (O (log (N))
Des tests
Récursif:
où ci-dessus la version récursive privée est appelée par la méthode publique suivante:
Solution en gardant une trace des chemins des deux nœuds:
où FindNodeAndPath est défini comme
BST (LCA) - non lié (juste pour compléter pour référence)
Tests unitaires
la source
Si quelqu'un s'intéresse au pseudo code (pour les travaux universitaires à domicile), en voici un.
la source
Bien que cela ait déjà été répondu, c'est mon approche de ce problème en utilisant le langage de programmation C. Bien que le code montre un arbre de recherche binaire (en ce qui concerne insert ()), mais l'algorithme fonctionne également pour un arbre binaire. L'idée est de passer en revue tous les nœuds qui se trouvent du nœud A au nœud B dans un parcours en ordre, de rechercher les indices pour ceux-ci dans le parcours de post-ordre. Le nœud avec l'index maximum dans le parcours post-ordre est le plus petit ancêtre commun.
Il s'agit d'un code C fonctionnel pour implémenter une fonction pour trouver le plus petit ancêtre commun dans un arbre binaire. Je propose également toutes les fonctions utilitaires, etc., mais passez à CommonAncestor () pour une compréhension rapide.
la source
Il peut y avoir une autre approche. Cependant, il n'est pas aussi efficace que celui déjà suggéré dans les réponses.
Créez un vecteur de chemin pour le nœud n1.
Créez un deuxième vecteur de chemin pour le nœud n2.
Vecteur de chemin impliquant l'ensemble des nœuds de celui-ci traverser pour atteindre le nœud en question.
Comparez les deux vecteurs de chemin. L'index où ils ne correspondent pas, retourne le nœud à cet index - 1. Cela donnerait l'ACV.
Inconvénients de cette approche:
Besoin de parcourir l'arbre deux fois pour calculer les vecteurs de chemin. Besoin d'espace O (h) supplémentaire pour stocker les vecteurs de chemin.
Cependant, cela est également facile à mettre en œuvre et à comprendre.
Code de calcul du vecteur de chemin:
la source
Essayez comme ça
la source
Manière brute:
Le problème avec la méthode ci-dessus est que nous allons faire la "recherche" plusieurs fois, c'est-à-dire qu'il y a une possibilité que chaque nœud soit traversé plusieurs fois. Nous pouvons surmonter ce problème si nous pouvons enregistrer les informations afin de ne pas les traiter à nouveau (pensez à la programmation dynamique).
Ainsi, plutôt que de rechercher chaque nœud, nous gardons une trace de ce qui a déjà été trouvé.
Meilleure façon:
Code:
la source
Code pour A Breadth First Search pour vous assurer que les deux nœuds sont dans l'arborescence. Alors seulement, avancez avec la recherche LCA. Veuillez commenter si vous avez des suggestions à améliorer. Je pense que nous pouvons probablement les marquer comme visités et redémarrer la recherche à un certain point où nous nous sommes arrêtés pour améliorer le deuxième nœud (s'il n'est pas trouvé VISITÉ)
la source
Vous avez raison de dire que sans nœud parent, une solution avec traversée vous donnera une complexité en temps O (n).
Approche transversale transversale Supposons que vous trouviez l'ACV pour les nœuds A et B, l'approche la plus simple consiste d'abord à obtenir le chemin de la racine à A, puis à obtenir le chemin de la racine à B.Une fois que vous avez ces deux chemins, vous pouvez facilement les parcourir. et trouvez le dernier nœud commun, qui est le plus petit ancêtre commun de A et B.
Solution récursive Une autre approche consiste à utiliser la récursivité. Tout d'abord, nous pouvons obtenir LCA à la fois de l'arbre gauche et de l'arbre droit (s'il existe). Si l'un de A ou B est le nœud racine, alors la racine est l'ACV et nous renvoyons simplement la racine, qui est le point final de la récursivité. Au fur et à mesure que nous divisons l'arbre en sous-arbres, nous atteindrons finalement A et B.
Pour combiner des solutions de sous-problèmes, si LCA (arbre de gauche) renvoie un nœud, nous savons que A et B se situent dans l'arbre de gauche et le nœud retourné est le résultat final. Si LCA (gauche) et LCA (droite) renvoient des nœuds non vides, cela signifie que A et B sont respectivement dans l'arborescence gauche et droite. Dans ce cas, le nœud racine est le nœud commun le plus bas.
Vérifiez le plus petit ancêtre commun pour une analyse détaillée et une solution.
la source
Certaines des solutions ici supposent qu'il existe une référence au nœud racine, d'autres supposent que l'arbre est un BST. Partager ma solution en utilisant hashmap, sans référence au
root
nœud et à l'arborescence peut être BST ou non-BST:la source
Solution 1: récursif - plus rapide
Solution 2: Itérative - Utilisation des pointeurs parents - Plus lente
la source