Erklären Sie Morris Inorder Tree Traversal, ohne Stapel oder Rekursion zu verwenden


125

Kann mir bitte jemand helfen, den folgenden Morris-Inorder-Tree-Traversal-Algorithmus zu verstehen, ohne Stapel oder Rekursion zu verwenden? Ich habe versucht zu verstehen, wie es funktioniert, aber es entgeht mir nur.

 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

Ich verstehe, dass der Baum so modifiziert ist, dass der current node, der right childaus dem max nodeIn gemacht wird, right subtreeund verwende diese Eigenschaft für die Inorder Traversal. Aber darüber hinaus bin ich verloren.

BEARBEITEN: Diesen zugehörigen C ++ - Code gefunden. Es fiel mir schwer zu verstehen, wie der Baum wiederhergestellt wird, nachdem er geändert wurde. Die Magie liegt in der elseKlausel, die getroffen wird, sobald das rechte Blatt geändert wird. Siehe Code für Details:

/* 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
Ich hatte noch nie von diesem Algorithmus gehört. Ganz elegant!
Fred Foo

5
Ich dachte, es könnte nützlich sein, die Quelle des Pseudocodes + Codes anzugeben (vermutlich).
Bernhard Barker


Im obigen Code ist die folgende Zeile nicht erforderlich: pre->right = NULL;
prashant.kr.mod

Antworten:


155

Wenn ich den Algorithmus richtig lese, sollte dies ein Beispiel dafür sein, wie er funktioniert:

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

Erstens Xist die Wurzel, also wird sie als initialisiert current. Xhat ein linkes Kind, wird also Xzum am weitesten rechts stehenden Kind des Xlinken Teilbaums gemacht - dem unmittelbaren Vorgänger Xeiner ungeordneten Durchquerung. So Xwird das richtige Kind daraus gemacht B, dann currentwird auf gesetzt Y. Der Baum sieht jetzt so aus:

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

(Y)oben bezieht sich auf Yund alle seine Kinder, die für Rekursionsprobleme weggelassen werden. Der wichtige Teil ist trotzdem aufgeführt. Nachdem der Baum eine Verknüpfung zu X hat, wird die Durchquerung fortgesetzt ...

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

Dann Awird ausgegeben, weil es kein linkes Kind hat, und es currentwird zu dem zurückgegeben Y, das Ain der vorherigen Iteration zum rechten Kind gemacht wurde. Bei der nächsten Iteration hat Y beide Kinder. Die Doppelbedingung der Schleife lässt sie jedoch anhalten, wenn sie sich selbst erreicht. Dies ist ein Hinweis darauf, dass der verbleibende Teilbaum bereits durchlaufen wurde. Also druckt es sich selbst und fährt mit seinem rechten Teilbaum fort, nämlich B.

Bdruckt selbst, und dann currentwird X, die durch die gleiche Prüfprozess geht wie Ytat, auch , dass sein linker Teilbaum durchlaufen wurde realisiert, mit der fort Z. Der Rest des Baumes folgt dem gleichen Muster.

Es ist keine Rekursion erforderlich, da anstelle des Zurückverfolgens durch einen Stapel eine Verknüpfung zurück zur Wurzel des (Unter-) Baums an den Punkt verschoben wird, an dem in einem rekursiven Inorder-Tree-Traversal-Algorithmus ohnehin darauf zugegriffen werden würde - nach dessen linker Teilbaum ist fertig.


3
Danke für die Erklärung. Das linke Kind wird nicht abgetrennt, sondern der Baum wird später wiederhergestellt, indem das neue rechte Kind abgetrennt wird, das zum Durchqueren dem Blatt ganz rechts hinzugefügt wird. Siehe meinen aktualisierten Beitrag mit dem Code.
Brainydexter

1
Schöne Skizze, aber ich verstehe die while-Schleifenbedingung immer noch nicht. Warum ist eine Überprüfung auf Vor-> Recht! = Strom erforderlich?
No_name

6
Ich verstehe nicht, warum das funktioniert. Nachdem Sie A gedruckt haben, wird Y zur Wurzel und Sie haben immer noch A als linkes Kind. Somit befinden wir uns in der gleichen Situation wie zuvor. Und wir wiederholen A. Tatsächlich sieht es aus wie eine Endlosschleife.
user678392

Trennt dies nicht die Verbindung zwischen Y und B? Wenn X als aktuell und Y als pre gesetzt ist, wird der rechte Teilbaum von pre nach unten geschaut, bis der Strom (X) gefunden wird, und dann wird pre => right als NULL gesetzt, was wäre B richtig? In Übereinstimmung mit dem oben angegebenen Code
Achint

17

Die rekursive In-Order-Durchquerung ist : (in-order(left)->key->in-order(right)). (Dies ist ähnlich wie bei DFS)

Wenn wir die DFS durchführen, müssen wir wissen, wohin wir zurückkehren müssen (deshalb behalten wir normalerweise einen Stapel).

Wenn wir einen übergeordneten Knoten durchlaufen, zu dem wir zurückverfolgen müssen ->, finden wir den Knoten, von dem wir zurückverfolgen müssen, und aktualisieren seine Verknüpfung zum übergeordneten Knoten.

Wann ziehen wir uns zurück? Wenn wir nicht weiter gehen können. Wann können wir nicht weiter gehen? Wenn kein linkes Kind anwesend ist.

Wohin ziehen wir zurück? Hinweis: an den NACHFOLGER!

Wenn wir also Knoten entlang des untergeordneten Pfades folgen, stellen Sie den Vorgänger bei jedem Schritt so ein, dass er auf den aktuellen Knoten zeigt. Auf diese Weise erhalten die Vorgänger Links zu Nachfolgern (ein Link zum Zurückverfolgen).

Wir folgen links, solange wir können, bis wir zurückgehen müssen. Wenn wir zurückverfolgen müssen, drucken wir den aktuellen Knoten und folgen dem richtigen Link zum Nachfolger.

Wenn wir gerade zurückgegangen sind -> müssen wir dem rechten Kind folgen (wir sind mit dem linken Kind fertig).

Wie kann man feststellen, ob wir gerade zurückgegangen sind? Holen Sie sich den Vorgänger des aktuellen Knotens und prüfen Sie, ob er eine richtige Verknüpfung (zu diesem Knoten) hat. Wenn ja - dann sind wir ihm gefolgt. Entfernen Sie den Link, um den Baum wiederherzustellen.

Wenn es keinen linken Link gab => haben wir nicht zurückverfolgt und sollten den linken Kindern folgen.

Hier ist mein Java-Code (Entschuldigung, es ist nicht 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
Ich mag Ihre Antwort sehr, weil sie die wichtigsten Gründe für die Entwicklung dieser Lösung liefert!
KFL

6

Ich habe hier eine Animation für den Algorithmus erstellt: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

Dies sollte hoffentlich zum Verständnis beitragen. Der blaue Kreis ist der Cursor und jede Folie ist eine Iteration der äußeren while-Schleife.

Hier ist der Code für Morris Traversal (ich habe ihn von Geeks für Geeks kopiert und geändert):

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)

Ihre Animation ist sehr interessant. Bitte überlegen Sie, daraus ein Bild zu machen, das in Ihren Beitrag aufgenommen wird, da externe Links häufig nach einiger Zeit absterben.
Laancelot

1
Die Animation ist hilfreich!
yyFred

Tolle Tabelle und Verwendung der Binärbaum-Bibliothek. Der Code ist jedoch nicht korrekt. Die Stammknoten können nicht gedruckt werden. Sie müssen print(cursor.value)nach pre.right = NoneZeile hinzufügen
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
            }        
        }
    }

Ich denke, dieser Code wäre besser zu verstehen. Verwenden Sie einfach eine Null, um Endlosschleifen zu vermeiden. Sie müssen keine andere Magie verwenden. Es kann leicht auf Vorbestellung geändert werden.


1
Die Lösung ist sehr ordentlich, aber es gibt ein Problem. Laut Knuth sollte der Baum am Ende nicht verändert werden. Dadurch geht temp.left = nullBaum verloren.
Ankur

Diese Methode kann beispielsweise zum Konvertieren eines Binärbaums in eine verknüpfte Liste verwendet werden.
Cyber_raj

Wie @Shan sagte, sollte der Algorithmus den ursprünglichen Baum nicht ändern. Während Ihr Algorithmus ihn durchläuft, zerstört er den ursprünglichen Baum. Daher unterscheidet sich dies tatsächlich vom ursprünglichen Algorithmus und ist daher irreführend.
ChaoSXDemon

2

Ich fand eine sehr gute bildliche Erklärung von Morris Traversal .

Morris Traversal


Nur-Link-Antworten verlieren ihren Wert, wenn der Link in Zukunft unterbrochen wird. Fügen Sie den entsprechenden Kontext aus dem Link in die Antwort ein.
Arun Vinoth

Sicher. Ich werde es bald hinzufügen.
Ashish Ranjan

1

Ich hoffe, der folgende Pseudocode ist aufschlussreicher:

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

Unter Bezugnahme auf den C ++ - Code in der Frage findet die innere while-Schleife den Vorgänger in der Reihenfolge des aktuellen Knotens. In einem Standard-Binärbaum muss das rechte untergeordnete Element des Vorgängers null sein, während in der Thread-Version das rechte untergeordnete Element auf den aktuellen Knoten zeigen muss. Wenn das rechte untergeordnete Element null ist, wird es auf den aktuellen Knoten gesetzt, wodurch effektiv das Threading erstellt wird , das als Rückgabepunkt verwendet wird, der andernfalls gespeichert werden müsste, normalerweise auf einem Stapel. Wenn das rechte untergeordnete Element nicht null ist, stellt der Algorithmus sicher, dass der ursprüngliche Baum wiederhergestellt wird, und setzt dann das Durchlaufen des rechten Teilbaums fort (in diesem Fall ist bekannt, dass der linke Teilbaum besucht wurde).


0

Python-Lösung Zeitkomplexität: O (n) Raumkomplexität: O (1)

Ausgezeichnete Morris Inorder Traversal Erklärung

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

Es tut mir leid, aber dies ist leider keine direkte Antwort auf die Frage. Das OP bat um eine Erklärung, wie es funktioniert, nicht um eine Implementierung, möglicherweise weil sie den Algorithmus selbst implementieren möchten. Ihre Kommentare sind gut für jemanden, der den Algorithmus bereits versteht, OP jedoch noch nicht. Außerdem sollten Antworten als Richtlinie in sich geschlossen sein, anstatt nur auf eine externe Ressource zu verlinken, da sich die Verknüpfung im Laufe der Zeit ändern oder unterbrechen kann. Es ist in Ordnung, Links einzuschließen, aber wenn Sie dies tun, sollten Sie auch mindestens den Kern dessen angeben, was der Link bereitstellt.
Anonymous1847

0

PFB Erklärung von 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;
        }
    }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.