web-dev-qa-db-fra.com

Expliquer la traversée de l'arbre inorder Morris sans utiliser de piles ou de récursivité

Quelqu'un peut-il m'aider à comprendre l'algorithme de traversée d'arbre inorder Morris suivant sans utiliser de piles ou de récursivité? J'essayais de comprendre comment cela fonctionne, mais ça m'échappe juste.

 1. Initialize current as root
 2. While current is not NULL
  If current does not have left child     
   a. Print current’s data
   b. Go to the right, i.e., current = current->right
  Else
   a. In current's left subtree, make current the right child of the rightmost node
   b. Go to this left child, i.e., current = current->left

Je comprends que l'arbre est modifié de manière à ce que le current node, se fait le right child du max node dans right subtree et utilisez cette propriété pour la traversée en ordre. Mais au-delà, je suis perdu.

EDIT: Trouvé le code c ++ qui l'accompagne. J'avais du mal à comprendre comment l'arbre est restauré après sa modification. La magie réside dans la clause else, qui est frappée une fois que la feuille de droite est modifiée. Voir le code pour plus de détails:

/* Function to traverse binary tree without recursion and
   without stack */
void MorrisTraversal(struct tNode *root)
{
  struct tNode *current,*pre;

  if(root == NULL)
     return; 

  current = root;
  while(current != NULL)
  {
    if(current->left == NULL)
    {
      printf(" %d ", current->data);
      current = current->right;
    }
    else
    {
      /* Find the inorder predecessor of current */
      pre = current->left;
      while(pre->right != NULL && pre->right != current)
        pre = pre->right;

      /* Make current as right child of its inorder predecessor */
      if(pre->right == NULL)
      {
        pre->right = current;
        current = current->left;
      }

     // MAGIC OF RESTORING the Tree happens here: 
      /* Revert the changes made in if part to restore the original
        tree i.e., fix the right child of predecssor */
      else
      {
        pre->right = NULL;
        printf(" %d ",current->data);
        current = current->right;
      } /* End of if condition pre->right == NULL */
    } /* End of if condition current->left == NULL*/
  } /* End of while */
}
94
brainydexter

Si je lis bien l'algorithme, cela devrait être un exemple de la façon dont il fonctionne:

     X
   /   \
  Y     Z
 / \   / \
A   B C   D

Tout d'abord, X est la racine, elle est donc initialisée comme current. X a un enfant gauche, donc X devient l'enfant le plus à droite du sous-arbre gauche de X - le prédécesseur immédiat de X dans une traversée inverse. Ainsi, X devient le bon enfant de B, puis current est défini sur Y. L'arbre ressemble maintenant à ceci:

    Y
   / \
  A   B
       \
        X
       / \
     (Y)  Z
         / \
        C   D

(Y) ci-dessus fait référence à Y et à tous ses enfants, qui sont omis pour les problèmes de récursivité. La partie importante est quand même répertoriée. Maintenant que l'arbre a un lien vers X, la traversée continue ...

 A
  \
   Y
  / \
(A)  B
      \
       X
      / \
    (Y)  Z
        / \
       C   D

Ensuite, A est sorti, car il n'a plus d'enfant à gauche, et current est retourné à Y, qui a été fait à droite de l'enfant de A dans l'itération précédente. À l'itération suivante, Y a les deux enfants. Cependant, la double condition de la boucle la fait s'arrêter lorsqu'elle atteint elle-même, ce qui indique que son sous-arbre gauche a déjà été traversé. Ainsi, il s'imprime et continue avec son sous-arbre droit, qui est B.

B s'imprime, puis current devient X, qui passe par le même processus de vérification que Y, tout en se rendant compte que son sous-arbre gauche a été traversé , en continuant avec Z. Le reste de l'arbre suit le même schéma.

Aucune récursivité n'est nécessaire, car au lieu de s'appuyer sur le retour en arrière à travers une pile, un lien vers la racine de la (sous-) arborescence est déplacé au point auquel il serait accessible dans un algorithme de traversée d'arbre récursif dans l'ordre de toute façon - après son le sous-arbre gauche est terminé.

135
Talonj

La traversée récursive dans l'ordre est: (in-order(left)->key->in-order(right)). (c'est similaire à DFS)

Lorsque nous faisons le DFS, nous devons savoir où revenir en arrière (c'est pourquoi nous gardons normalement une pile).

Lorsque nous traversons un nœud parent vers lequel nous devons revenir en arrière -> nous trouvons le nœud à partir duquel nous devons revenir en arrière et mettre à jour son lien vers le nœud parent.

Quand on revient en arrière? Quand on ne peut pas aller plus loin. Quand on ne peut pas aller plus loin? Quand aucun enfant n'est parti présent.

Où revenons-nous? Avis: au SUCCESSEUR!

Ainsi, lorsque nous suivons les nœuds le long du chemin enfant gauche, définissez le prédécesseur à chaque étape pour pointer vers le nœud actuel. De cette façon, les prédécesseurs auront des liens vers les successeurs (un lien pour le retour en arrière).

Nous suivons à gauche pendant que nous le pouvons jusqu'à ce que nous ayons besoin de revenir en arrière. Lorsque nous devons revenir en arrière, nous imprimons le nœud actuel et suivons le lien droit vers le successeur.

Si nous venons de faire marche arrière -> nous devons suivre le bon enfant (nous avons fini avec l'enfant de gauche).

Comment savoir si nous venons de faire marche arrière? Obtenez le prédécesseur du nœud actuel et vérifiez s'il a un lien correct (vers ce nœud). Si c'est le cas - alors nous l'avons suivi. supprimez le lien pour restaurer l'arborescence.

S'il n'y avait pas de lien gauche => nous n'avons pas fait marche arrière et nous devrions continuer à suivre les enfants de gauche.

Voici mon Java (Désolé, ce n'est pas C++)

public static <T> List<T> traverse(Node<T> bstRoot) {
    Node<T> current = bstRoot;
    List<T> result = new ArrayList<>();
    Node<T> prev = null;
    while (current != null) {
        // 1. we backtracked here. follow the right link as we are done with left sub-tree (we do left, then right)
        if (weBacktrackedTo(current)) {
            assert prev != null;
            // 1.1 clean the backtracking link we created before
            prev.right = null;
            // 1.2 output this node's key (we backtrack from left -> we are finished with left sub-tree. we need to print this node and go to right sub-tree: inOrder(left)->key->inOrder(right)
            result.add(current.key);
            // 1.15 move to the right sub-tree (as we are done with left sub-tree).
            prev = current;
            current = current.right;
        }
        // 2. we are still tracking -> going deep in the left
        else {
            // 15. reached sink (the leftmost element in current subtree) and need to backtrack
            if (needToBacktrack(current)) {
                // 15.1 return the leftmost element as it's the current min
                result.add(current.key);
                // 15.2 backtrack:
                prev = current;
                current = current.right;
            }
            // 4. can go deeper -> go as deep as we can (this is like dfs!)
            else {
                // 4.1 set backtracking link for future use (this is one of parents)
                setBacktrackLinkTo(current);
                // 4.2 go deeper
                prev = current;
                current = current.left;
            }
        }
    }
    return result;
}

private static <T> void setBacktrackLinkTo(Node<T> current) {
    Node<T> predecessor = getPredecessor(current);
    if (predecessor == null) return;
    predecessor.right = current;
}

private static boolean needToBacktrack(Node current) {
    return current.left == null;
}

private static <T> boolean weBacktrackedTo(Node<T> current) {
    Node<T> predecessor = getPredecessor(current);
    if (predecessor == null) return false;
    return predecessor.right == current;
}

private static <T> Node<T> getPredecessor(Node<T> current) {
    // predecessor of current is the rightmost element in left sub-tree
    Node<T> result = current.left;
    if (result == null) return null;
    while(result.right != null
            // this check is for the case when we have already found the predecessor and set the successor of it to point to current (through right link)
            && result.right != current) {
        result = result.right;
    }
    return result;
}
9
Maria Sakharova
public static void morrisInOrder(Node root) {
        Node cur = root;
        Node pre;
        while (cur!=null){
            if (cur.left==null){
                System.out.println(cur.value);      
                cur = cur.right; // move to next right node
            }
            else {  // has a left subtree
                pre = cur.left;
                while (pre.right!=null){  // find rightmost
                    pre = pre.right;
                }
                pre.right = cur;  // put cur after the pre node
                Node temp = cur;  // store cur node
                cur = cur.left;  // move cur to the top of the new tree
                temp.left = null;   // original cur left be null, avoid infinite loops
            }        
        }
    }

Je pense que ce code serait mieux à comprendre, utilisez simplement un null pour éviter les boucles infinies, ne pas utiliser d'autre magie. Il peut être facilement modifié en précommande.

3
Adeath

J'espère que le pseudo-code ci-dessous est plus révélateur:

node = root
while node != null
    if node.left == null
        visit the node
        node = node.right
    else
        let pred_node be the inorder predecessor of node
        if pred_node.right == null /* create threading in the binary tree */
            pred_node.right = node
            node = node.left
        else         /* remove threading from the binary tree */
            pred_node.right = null 
            visit the node
            node = node.right

En se référant au code C++ dans la question, la boucle while intérieure trouve le prédécesseur en ordre du nœud actuel. Dans un arbre binaire standard, le bon enfant du prédécesseur doit être nul, tandis que dans la version filetée, le bon enfant doit pointer vers le nœud actuel. Si le bon enfant est nul, il est défini sur le nœud actuel, créant effectivement le threading , qui est utilisé comme point de retour qui devrait autrement être stocké, généralement sur une pile. Si le bon enfant n'est pas nul, l'algorithme s'assure que l'arborescence d'origine est restaurée, puis continue la traversée dans le sous-arbre droit (dans ce cas on sait que le sous-arbre gauche a été visité).

1
EXP