Spiega l'attraversamento dell'albero in ordine di Morris senza usare pile o ricorsione


126

Qualcuno può aiutarmi a capire il seguente algoritmo di attraversamento dell'albero inordine di Morris senza usare stack o ricorsione? Stavo cercando di capire come funziona, ma mi sta sfuggendo.

 1. Initialize current as root
 2. While current is not NULL
  If current does not have left child     
   a. Print currents 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

Capisco che l'albero sia modificato in modo che il current node, sia composto right childda max nodein right subtreee usi questa proprietà per l'attraversamento inordine. Ma oltre a questo, mi sono perso.

EDIT: trovato questo codice c ++ di accompagnamento. Ho avuto difficoltà a capire come viene ripristinato l'albero dopo che è stato modificato. La magia sta nella elseclausola, che viene colpita una volta modificata la foglia destra. Vedere il codice per i dettagli:

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

12
Non avevo mai sentito parlare di questo algoritmo prima. Abbastanza elegante!
Fred Foo

5
Ho pensato che potesse essere utile indicare la fonte dello pseudo-codice + codice (presumibilmente).
Bernhard Barker


nel codice sopra, la seguente riga non è richiesta: pre->right = NULL;
prashant.kr.mod

Risposte:


155

Se sto leggendo correttamente l'algoritmo, questo dovrebbe essere un esempio di come funziona:

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

Primo, Xè la radice, quindi è inizializzato come current. Xha un figlio a sinistra, quindi Xviene reso il figlio più a destra del Xsottoalbero sinistro di - l'immediato predecessore di Xin un attraversamento in ordine. Quindi Xviene creato il figlio giusto di B, quindi currentviene impostato Y. L'albero ora ha questo aspetto:

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

(Y)sopra si riferisce a Ye tutti i suoi figli, che vengono omessi per problemi di ricorsione. La parte importante è comunque elencata. Ora che l'albero ha un collegamento a X, l'attraversamento continua ...

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

Quindi Aviene emesso, perché non ha alcun figlio sinistro, e currentviene restituito al figlio destro Ycreato Anell'iterazione precedente. Nella successiva iterazione, Y ha entrambi i figli. Tuttavia, la doppia condizione del loop lo fa fermare quando raggiunge se stesso, il che è un'indicazione che la sua sottostruttura sinistra è già stata attraversata. Quindi, si stampa da solo e continua con la sua sottostruttura destra, che è B.

Bstampa se stesso, e quindi currentdiventa X, che passa attraverso lo stesso processo di controllo come ha Yfatto, rendendosi conto anche che il suo sottoalbero sinistro è stato attraversato, continuando con il Z. Il resto dell'albero segue lo stesso schema.

Non è necessaria alcuna ricorsione, perché invece di fare affidamento sul backtracking attraverso uno stack, un collegamento alla radice del (sotto) albero viene spostato al punto in cui sarebbe comunque accessibile in un algoritmo ricorsivo di attraversamento dell'albero inordine - dopo il suo la sottostruttura sinistra è terminata.


3
Grazie per la spiegazione. Il bambino sinistro non viene reciso, invece l'albero viene ripristinato in seguito recidendo il nuovo bambino destro che viene aggiunto alla foglia più a destra ai fini dell'attraversamento. Vedi il mio post aggiornato con il codice.
brainydexter

1
Bello schizzo, ma ancora non capisco la condizione del ciclo while. Perché è necessario controllare la pre-> destra! = Corrente?
No_name

6
Non vedo perché funziona. Dopo aver stampato A, Y diventa la radice e hai ancora A come figlio sinistro. Quindi, siamo nella stessa situazione di prima. E ripetiamo A. In effetti, sembra un ciclo infinito.
user678392

Questo non interrompe la connessione tra Y e B? Quando X è impostato come corrente e Y è impostato come pre, allora guarderà in basso nel sottoalbero destro di pre fino a trovare la corrente (X), e quindi imposta pre => proprio come NULL, quale sarebbe B giusto? Secondo il codice pubblicato sopra
Achint

17

Il ricorsiva attraversamento in-ordine è: (in-order(left)->key->in-order(right)). (questo è simile a DFS)

Quando eseguiamo il DFS, dobbiamo sapere dove tornare indietro (ecco perché normalmente teniamo uno stack).

Mentre passiamo attraverso un nodo genitore a cui dovremo tornare indietro -> troviamo il nodo da cui avremo bisogno di tornare indietro e aggiornare il suo collegamento al nodo genitore.

Quando torniamo indietro? Quando non possiamo andare oltre. Quando non possiamo andare oltre? Quando nessun bambino sinistro è presente.

Dove torniamo indietro? Avviso: a SUCCESSOR!

Quindi, mentre seguiamo i nodi lungo il percorso figlio sinistro, imposta il predecessore in ogni passaggio in modo che punti al nodo corrente. In questo modo, i predecessori avranno collegamenti ai successori (un collegamento per il backtracking).

Seguiamo a sinistra finché possiamo finché non abbiamo bisogno di tornare sui nostri passi. Quando abbiamo bisogno di tornare indietro, stampiamo il nodo corrente e seguiamo il collegamento corretto al successore.

Se siamo appena tornati indietro -> dobbiamo seguire il bambino destro (abbiamo finito con il bambino sinistro).

Come capire se siamo appena tornati indietro? Ottieni il predecessore del nodo corrente e controlla se ha un collegamento corretto (a questo nodo). Se è successo, allora l'abbiamo seguito. rimuovere il collegamento per ripristinare l'albero.

Se non c'era alcun collegamento sinistro => non siamo tornati indietro e dovremmo procedere seguendo i bambini di sinistra.

Ecco il mio codice Java (scusa, non è 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;
}

4
Mi piace molto la tua risposta perché fornisce il ragionamento di alto livello per arrivare a questa soluzione!
KFL

6

Ho creato un'animazione per l'algoritmo qui: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

Si spera che questo dovrebbe aiutare a capire. Il cerchio blu è il cursore e ogni diapositiva è un'iterazione del ciclo while esterno.

Ecco il codice per morris traversal (l'ho copiato e modificato da geek per geek):

def MorrisTraversal(root):
    # Set cursor to root of binary tree
    cursor = root
    while cursor is not None:
        if cursor.left is None:
            print(cursor.value)
            cursor = cursor.right
        else:
            # Find the inorder predecessor of cursor
            pre = cursor.left
            while True:
                if pre.right is None:
                    pre.right = cursor
                    cursor = cursor.left
                    break
                if pre.right is cursor:
                    pre.right = None
                    cursor = cursor.right
                    break
                pre = pre.right
#And now for some tests. Try "pip3 install binarytree" to get the needed package which will visually display random binary trees
import binarytree as b
for _ in range(10):
    print()
    print("Example #",_)
    tree=b.tree()
    print(tree)
    MorrisTraversal(tree)

La tua animazione è piuttosto interessante. Considera l'idea di trasformarlo in un'immagine da includere nel tuo post, poiché i link esterni spesso muoiono dopo un po 'di tempo.
laancelot

1
L'animazione è utile!
yyFred

ottimo foglio di calcolo e utilizzo della libreria binarytree. ma il codice non è corretto, non riesce a stampare i nodi root. è necessario aggiungere print(cursor.value)dopo la pre.right = Noneriga
satnam

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

Penso che questo codice sarebbe meglio capire, basta usare un null per evitare loop infiniti, non è necessario usare la magia altro. Può essere facilmente modificato in preordine.


1
La soluzione è molto chiara ma c'è un problema. Secondo Knuth l'albero non dovrebbe essere modificato alla fine. Facendo l' temp.left = nullalbero andrà perso.
Ankur

Questo metodo può essere utilizzato in luoghi come la conversione di un albero binario in un elenco collegato.
cyber_raj

Come quello che ha detto @Shan, l'algoritmo non dovrebbe alterare l'albero originale. Mentre il tuo algoritmo funziona per attraversarlo, distrugge l'albero originale. Pertanto, questo è effettivamente diverso dall'algoritmo originale e quindi fuorviante.
ChaoSXDemon


1

Spero che lo pseudo codice di seguito sia più rivelatore:

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

Facendo riferimento al codice C ++ nella domanda, il ciclo while interno trova il predecessore in ordine del nodo corrente. In un albero binario standard, il figlio destro del predecessore deve essere nullo, mentre nella versione con thread il figlio destro deve puntare al nodo corrente. Se il figlio destro è nullo, viene impostato sul nodo corrente, creando effettivamente il threading , che viene utilizzato come punto di ritorno che altrimenti dovrebbe essere memorizzato, di solito su uno stack. Se il figlio destro non è nullo, l'algoritmo si assicura che l'albero originale venga ripristinato e quindi continua l'attraversamento nella sottostruttura destra (in questo caso è noto che la sottostruttura sinistra è stata visitata).


0

Complessità temporale della soluzione Python: O (n) Complessità spaziale: O (1)

Eccellente Morris Inorder Traversal Explanation

class Solution(object):
def inorderTraversal(self, current):
    soln = []
    while(current is not None):    #This Means we have reached Right Most Node i.e end of LDR traversal

        if(current.left is not None):  #If Left Exists traverse Left First
            pre = current.left   #Goal is to find the node which will be just before the current node i.e predecessor of current node, let's say current is D in LDR goal is to find L here
            while(pre.right is not None and pre.right != current ): #Find predecesor here
                pre = pre.right
            if(pre.right is None):  #In this case predecessor is found , now link this predecessor to current so that there is a path and current is not lost
                pre.right = current
                current = current.left
            else:                   #This means we have traverse all nodes left to current so in LDR traversal of L is done
                soln.append(current.val) 
                pre.right = None       #Remove the link tree restored to original here 
                current = current.right
        else:               #In LDR  LD traversal is done move to R  
            soln.append(current.val)
            current = current.right

    return soln

Mi dispiace, ma questa purtroppo non è una risposta diretta alla domanda. L'OP ha chiesto una spiegazione per come funziona, non un'implementazione, forse perché vogliono implementare l'algoritmo da soli. I tuoi commenti sono utili per qualcuno che capisce già l'algoritmo, ma OP ancora no. Inoltre, come politica, le risposte dovrebbero essere autonome invece di collegarsi semplicemente a una risorsa esterna, perché il collegamento potrebbe cambiare o interrompersi nel tempo. Va bene includere i collegamenti, ma se lo fai, dovresti anche includere almeno l'essenza di ciò che il collegamento fornisce.
Anonimo

0

PFB Spiegazione di Morris In-order Traversal.

  public class TreeNode
    {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val = 0, TreeNode left = null, TreeNode right = null)
        {
            this.val = val;
            this.left = left;
            this.right = right;
        }
    }

    class MorrisTraversal
    {
        public static IList<int> InOrderTraversal(TreeNode root)
        {
            IList<int> list = new List<int>();
            var current = root;
            while (current != null)
            {
                //When there exist no left subtree
                if (current.left == null)
                {
                    list.Add(current.val);
                    current = current.right;
                }
                else
                {
                    //Get Inorder Predecessor
                    //In Order Predecessor is the node which will be printed before
                    //the current node when the tree is printed in inorder.
                    //Example:- {1,2,3,4} is inorder of the tree so inorder predecessor of 2 is node having value 1
                    var inOrderPredecessorNode = GetInorderPredecessor(current);
                    //If the current Predeccessor right is the current node it means is already printed.
                    //So we need to break the thread.
                    if (inOrderPredecessorNode.right != current)
                    {
                        inOrderPredecessorNode.right = null;
                        list.Add(current.val);
                        current = current.right;
                    }//Creating thread of the current node with in order predecessor.
                    else
                    {
                        inOrderPredecessorNode.right = current;
                        current = current.left;
                    }
                }
            }

            return list;
        }

        private static TreeNode GetInorderPredecessor(TreeNode current)
        {
            var inOrderPredecessorNode = current.left;
            //Finding Extreme right node of the left subtree
            //inOrderPredecessorNode.right != current check is added to detect loop
            while (inOrderPredecessorNode.right != null && inOrderPredecessorNode.right != current)
            {
                inOrderPredecessorNode = inOrderPredecessorNode.right;
            }

            return inOrderPredecessorNode;
        }
    }
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.