อธิบายการข้ามต้นไม้ตามลำดับของมอร์ริสโดยไม่ต้องใช้สแต็คหรือการเรียกซ้ำ


127

ใครช่วยกรุณาช่วยฉันทำความเข้าใจอัลกอริทึมการเดินทางข้ามต้นไม้ตามลำดับมอร์ริสต่อไปนี้โดยไม่ต้องใช้สแต็กหรือการเรียกซ้ำ ฉันพยายามเข้าใจว่ามันทำงานอย่างไร แต่มันก็หนีฉันไป

 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

ฉันเข้าใจว่าต้นไม้ได้รับการแก้ไขในลักษณะที่current nodeสร้างขึ้นright childจากmax nodein right subtreeและใช้คุณสมบัตินี้สำหรับการข้ามผ่านแบบเรียงลำดับ แต่นอกเหนือจากนั้นฉันหลงทาง

แก้ไข: พบโค้ด c ++ ที่มาพร้อมนี้ ฉันมีช่วงเวลาที่ยากลำบากในการทำความเข้าใจว่าต้นไม้ได้รับการแก้ไขอย่างไรหลังจากแก้ไขแล้ว เวทมนตร์อยู่ในelseประโยคซึ่งจะถูกตีเมื่อมีการแก้ไขใบไม้ที่เหมาะสม ดูรหัสสำหรับรายละเอียด:

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

13
ฉันไม่เคยได้ยินอัลกอริทึมนี้มาก่อน สวยหรูเลยทีเดียว!
Fred Foo

5
ฉันคิดว่าการระบุแหล่งที่มาของรหัส + รหัสเทียมอาจมีประโยชน์(น่าจะเป็น)
Bernhard Barker

1
ที่มา: geeksforgeeks.org/…
DebashisDeb

ในโค้ดด้านบนไม่จำเป็นต้องใช้บรรทัดต่อไปนี้: pre->right = NULL;
prashant.kr.mod

คำตอบ:


155

หากฉันอ่านอัลกอริทึมถูกต้องนี่ควรเป็นตัวอย่างวิธีการทำงาน:

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

ประการแรกXคือรากดังนั้นจึงเริ่มต้นเป็นcurrent. Xมีลูกทางซ้ายดังนั้นจึงXถูกสร้างให้เป็นลูกทางขวาสุดของXแผนผังย่อยทางซ้ายของต้นไม้ซึ่งเป็นบรรพบุรุษที่Xอยู่ติดกับในการข้ามผ่านตามลำดับ ดังนั้นXจะทำเด็กทางขวาของBแล้วมีการตั้งค่าcurrent Yตอนนี้ต้นไม้มีลักษณะดังนี้:

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

(Y)ข้างต้นหมายถึงYและลูก ๆ ทั้งหมดซึ่งถูกละไว้สำหรับปัญหาการเรียกซ้ำ ส่วนที่สำคัญแสดงอยู่แล้ว ตอนนี้ต้นไม้มีลิงค์กลับไปที่ X แล้วการข้ามผ่านยังคงดำเนินต่อไป ...

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

จากนั้นAจะถูกส่งออกเพราะมันไม่มีลูกด้านซ้ายและcurrentจะถูกส่งกลับไปYซึ่งถูกสร้างขึ้นAเป็นลูกที่เหมาะสมในการทำซ้ำก่อนหน้านี้ ในการทำซ้ำครั้งต่อไป Y มีทั้งลูก อย่างไรก็ตามเงื่อนไขคู่ของลูปจะหยุดเมื่อมันมาถึงตัวมันเองซึ่งเป็นข้อบ่งชี้ว่าทรีย่อยด้านซ้ายได้ถูกข้ามผ่านไปแล้ว Bดังนั้นมันพิมพ์เองและยังคงมีทรีย่อยทางขวาซึ่งเป็น

Bพิมพ์ตัวเองจากนั้นcurrentจะกลายเป็นXซึ่งต้องผ่านขั้นตอนการตรวจสอบเช่นเดียวกับที่Yทำโดยตระหนักว่าทรีย่อยด้านซ้ายของมันถูกข้ามไปแล้วดำเนินการต่อด้วยไฟล์Z. ส่วนที่เหลือของต้นไม้เป็นไปตามรูปแบบเดียวกัน

ไม่จำเป็นต้องมีการเรียกซ้ำเพราะแทนที่จะใช้การย้อนกลับผ่านสแต็กลิงก์กลับไปยังรากของทรี (ย่อย) จะถูกย้ายไปยังจุดที่จะเข้าถึงได้ในอัลกอริทึมการข้ามผ่านต้นไม้เรียงลำดับแบบวนซ้ำอยู่ดี - หลังจากมัน แผนผังย่อยด้านซ้ายเสร็จสิ้น


3
ขอบคุณสำหรับคำอธิบาย เด็กทางซ้ายจะไม่ถูกตัดออก แต่ต้นไม้จะได้รับการบูรณะในภายหลังโดยการตัดลูกด้านขวาใหม่ที่เพิ่มเข้าไปในใบไม้ขวาสุดเพื่อจุดประสงค์ในการข้ามผ่าน ดูโพสต์ที่อัปเดตของฉันพร้อมรหัส
brainydexter

1
ร่างดี แต่ฉันยังไม่เข้าใจเงื่อนไข while loop เหตุใดการตรวจสอบ pre-> right! = current จึงจำเป็น?
No_name

6
ฉันไม่เห็นว่าทำไมถึงได้ผล หลังจากที่คุณพิมพ์ A แล้ว Y จะกลายเป็นรูทและคุณยังมี A เป็นลูกทางซ้าย ดังนั้นเราจึงตกอยู่ในสถานการณ์เดียวกันกับที่ผ่านมา และเราทำซ้ำ A. อันที่จริงมันดูเหมือนวนซ้ำไม่สิ้นสุด
user678392

สิ่งนี้ไม่ได้ตัดการเชื่อมต่อระหว่าง Y และ B หรือไม่? เมื่อ X ถูกตั้งค่าเป็นปัจจุบันและ Y ถูกตั้งค่าไว้ล่วงหน้ามันจะมองลงไปที่ทรีย่อยที่ถูกต้องของ pre จนกว่าจะพบปัจจุบัน (X) จากนั้นตั้งค่าก่อน => เป็นโมฆะซึ่งจะเป็น B ใช่หรือไม่ ตามรหัสที่โพสต์ด้านบน
Achint

17

traversal recursive (in-order(left)->key->in-order(right))ในการสั่งซื้อคือ (คล้ายกับ DFS)

เมื่อเราทำ DFS เราจำเป็นต้องรู้ว่าจะย้อนกลับไปที่ใด (นั่นคือเหตุผลที่เรามักจะเก็บสแต็กไว้)

เมื่อเราผ่านโหนดแม่ที่เราจะต้องย้อนกลับไปที่ -> เราพบโหนดที่เราจะต้องย้อนกลับและอัปเดตลิงก์ไปยังโหนดหลัก

เมื่อเราย้อนรอย? เมื่อเราไปต่อไม่ได้. เมื่อเราไปต่อไม่ได้? เมื่อของขวัญของเด็กไม่เหลือ

เราย้อนรอยไปที่ใด หมายเหตุ: ถึง SUCCESSOR!

ดังนั้นในขณะที่เราติดตามโหนดตามเส้นทางซ้าย - ลูกให้ตั้งค่าบรรพบุรุษในแต่ละขั้นตอนให้ชี้ไปที่โหนดปัจจุบัน วิธีนี้รุ่นก่อนจะมีลิงก์ไปยังผู้สืบทอด (ลิงก์สำหรับการย้อนรอย)

เราตามซ้ายในขณะที่ทำได้จนกว่าเราจะต้องย้อนรอย เมื่อเราต้องการย้อนกลับเราจะพิมพ์โหนดปัจจุบันและไปตามลิงค์ด้านขวาไปยังตัวตายตัวแทน

หากเราเพิ่งย้อนรอย -> เราจำเป็นต้องติดตามลูกที่ถูกต้อง (เราทำด้วยลูกทางซ้าย)

จะทราบได้อย่างไรว่าเราเพิ่งย้อนรอย? รับโหนดก่อนหน้าและตรวจสอบว่ามีลิงค์ที่ถูกต้อง (ไปยังโหนดนี้) หรือไม่ หากมี - เกินกว่าที่เราจะทำตาม ลบลิงค์เพื่อกู้คืนทรี

หากไม่มีลิงก์ด้านซ้าย => เราไม่ได้ย้อนกลับและควรดำเนินการตามเด็กที่เหลือ

นี่คือรหัส Java ของฉัน (ขออภัยไม่ใช่ 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
ฉันชอบคำตอบของคุณมากเพราะมันให้เหตุผลระดับสูงในการหาวิธีแก้ปัญหานี้!
KFL

6

ฉันได้สร้างภาพเคลื่อนไหวสำหรับอัลกอริทึมที่นี่: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

หวังว่าจะช่วยให้เข้าใจ วงกลมสีน้ำเงินคือเคอร์เซอร์และแต่ละสไลด์จะเป็นการวนซ้ำของด้านนอกในขณะที่วนซ้ำ

นี่คือรหัสสำหรับ morris traversal (ฉันคัดลอกและแก้ไขจาก geeks for geeks):

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)

ภาพเคลื่อนไหวของคุณน่าสนใจมากทีเดียว โปรดพิจารณาทำให้เป็นรูปภาพที่จะรวมอยู่ในโพสต์ของคุณเนื่องจากลิงก์ภายนอกมักจะหายไปหลังจากผ่านไประยะหนึ่ง
laancelot

1
ภาพเคลื่อนไหวมีประโยชน์!
yyFred

สเปรดชีตที่ยอดเยี่ยมและการใช้งานไลบรารี binarytree แต่รหัสไม่ถูกต้องพิมพ์โหนดรากไม่สำเร็จ คุณจำเป็นต้องเพิ่มprint(cursor.value)หลังจากpre.right = Noneบรรทัด
เนม

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

ฉันคิดว่ารหัสนี้น่าจะเข้าใจได้ดีขึ้นเพียงใช้ null เพื่อหลีกเลี่ยงการวนซ้ำที่ไม่มีที่สิ้นสุดไม่ต้องใช้เวทมนตร์อื่น สามารถแก้ไขได้อย่างง่ายดายเพื่อสั่งซื้อล่วงหน้า


1
การแก้ปัญหาเป็นไปอย่างเรียบร้อย แต่มีปัญหาอย่างหนึ่ง ตาม Knuth ต้นไม้ไม่ควรแก้ไขในที่สุด โดยการทำtemp.left = nullต้นไม้จะหายไป
Ankur

วิธีนี้สามารถใช้ในสถานที่ต่างๆเช่นการแปลงต้นไม้ไบนารีเป็นรายการที่เชื่อมโยง
cyber_raj

เช่นเดียวกับสิ่งที่ @Shan กล่าวว่าอัลกอริทึมไม่ควรเปลี่ยนแปลงทรีดั้งเดิม ในขณะที่อัลกอริทึมของคุณทำงานเพื่อข้ามผ่านมันก็ทำลายทรีดั้งเดิม ดังนั้นนี่จึงแตกต่างจากอัลกอริทึมดั้งเดิมจริง ๆ จึงทำให้เข้าใจผิด
ChaoSXDemon

2

ผมพบว่าคำอธิบายภาพที่ดีมากของมอร์ริสการข้ามผ่าน

มอร์ริส Traversal


คำตอบเฉพาะลิงก์จะสูญเสียคุณค่าเมื่อลิงก์เสียในอนาคตโปรดเพิ่มบริบทที่เกี่ยวข้องจากลิงก์เป็นคำตอบ
อรุณวิโน ธ

แน่ใจ ฉันจะเพิ่มเร็ว ๆ นี้
Ashish Ranjan

1

ฉันหวังว่าโค้ดหลอกด้านล่างจะเปิดเผยมากขึ้น:

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

อ้างอิงถึงรหัส C ++ ในคำถามวงใน while จะค้นหาลำดับก่อนหน้าของโหนดปัจจุบัน ในไบนารีทรีมาตรฐานชายด์ที่เหมาะสมของรุ่นก่อนต้องเป็นโมฆะในขณะที่ในเวอร์ชันเธรดลูกที่ถูกต้องต้องชี้ไปที่โหนดปัจจุบัน หากชายด์ที่ถูกต้องเป็นโมฆะจะถูกตั้งค่าเป็นโหนดปัจจุบันสร้างเธรดได้อย่างมีประสิทธิภาพซึ่งใช้เป็นจุดส่งคืนที่จะต้องถูกเก็บไว้โดยปกติจะอยู่บนสแต็ก หากลูกที่ถูกต้องไม่เป็นโมฆะอัลกอริทึมจะตรวจสอบให้แน่ใจว่าทรีดั้งเดิมได้รับการกู้คืนแล้วจากนั้นจะเคลื่อนที่ต่อไปในทรีย่อยด้านขวา (ในกรณีนี้จะทราบว่ามีการเยี่ยมชมทรีย่อยด้านซ้าย)


0

Python Solution Time Complexity: O (n) Space Complexity: O (1)

คำอธิบายการเดินทางข้ามผ่าน Morris Inorder ที่ยอดเยี่ยม

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

ฉันขอโทษ แต่น่าเสียดายที่นี่ไม่ใช่คำตอบโดยตรงสำหรับคำถาม OP ขอคำอธิบายวิธีการทำงานไม่ใช่การใช้งานอาจเป็นเพราะพวกเขาต้องการใช้อัลกอริทึมด้วยตนเอง ความคิดเห็นของคุณดีสำหรับคนที่เข้าใจอัลกอริทึมอยู่แล้ว แต่ OP ยังไม่เข้าใจ นอกจากนี้ตามนโยบายแล้วคำตอบควรมีอยู่ในตัวแทนที่จะลิงก์ไปยังแหล่งข้อมูลภายนอกบางส่วนเท่านั้นเนื่องจากลิงก์อาจเปลี่ยนแปลงหรือพังเมื่อเวลาผ่านไป คุณสามารถใส่ลิงก์ได้ แต่ถ้าเป็นเช่นนั้นคุณควรใส่ส่วนสำคัญของสิ่งที่ลิงก์นั้นให้ด้วย
Anonymous1847

0

คำอธิบาย PFB ของ 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;
        }
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.