Giải thích việc duyệt qua cây inorder của Morris mà không sử dụng ngăn xếp hoặc đệ quy


125

Ai đó có thể vui lòng giúp tôi hiểu thuật toán duyệt cây Morris inorder sau đây mà không sử dụng ngăn xếp hoặc đệ quy không? Tôi đã cố gắng hiểu cách nó hoạt động, nhưng nó chỉ thoát khỏi tôi.

 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

Tôi hiểu rằng cây được sửa đổi theo cách mà current node, được tạo thành right childcủa bên max nodetrong right subtreevà sử dụng thuộc tính này cho việc chuyển tải không qua đăng ký. Nhưng ngoài ra, tôi lạc lối.

CHỈNH SỬA: Đã tìm thấy mã c ++ đi kèm này. Tôi đã rất khó hiểu cách cây được phục hồi sau khi nó được sửa đổi. Điều kỳ diệu nằm ở elsemệnh đề, được đánh khi lá bên phải được sửa đổi. Xem mã để biết chi tiết:

/* 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
Tôi chưa bao giờ nghe nói về thuật toán này trước đây. Khá thanh lịch!
Fred Foo,

5
Tôi nghĩ có thể hữu ích khi chỉ ra nguồn của mã giả + mã (có lẽ).
Bernhard Barker


trong đoạn mã trên, dòng sau là không bắt buộc: pre->right = NULL;
prashant.kr.mod

Câu trả lời:


155

Nếu tôi đang đọc đúng thuật toán, đây sẽ là một ví dụ về cách nó hoạt động:

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

Đầu tiên, Xlà gốc, vì vậy nó được khởi tạo như current. Xcó một con bên trái, vì vậy Xđược đặt làm con ngoài cùng bên phải của Xcây con bên trái - tiền thân ngay lập tức Xtrong một đường ngang nhỏ hơn. Vì vậy, Xđược làm đúng con của B, sau đó currentđược đặt thành Y. Cây bây giờ trông như thế này:

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

(Y)ở trên đề cập đến Yvà tất cả các con của nó, được bỏ qua cho các vấn đề đệ quy. Phần quan trọng vẫn được liệt kê. Bây giờ cây có liên kết trở lại X, quá trình truyền tải tiếp tục ...

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

Sau đó Ađược xuất ra, bởi vì nó không có con bên trái, và currentđược trả về Y, được tạo Athành con bên phải trong lần lặp trước. Ở lần lặp tiếp theo, Y có cả hai con. Tuy nhiên, điều kiện kép của vòng lặp làm cho nó dừng lại khi nó đạt đến chính nó, đó là một dấu hiệu cho thấy cây con bên trái của nó đã được duyệt qua. Vì vậy, nó tự in và tiếp tục với cây con bên phải của nó, đó là B.

Btự in, và sau đó currenttrở thành X, trải qua quá trình kiểm tra tương tự như Yđã làm, cũng nhận ra rằng cây con bên trái của nó đã được duyệt, tiếp tục với Z. Phần còn lại của cây theo cùng một mô hình.

Không cần đệ quy, bởi vì thay vì dựa vào backtracking thông qua một ngăn xếp, một liên kết trở lại gốc của cây (con) được di chuyển đến điểm mà tại đó nó sẽ được truy cập trong thuật toán duyệt qua cây Inorder đệ quy - dù sao thì cây con bên trái đã kết thúc.


3
Cảm ơn vì lời giải thích. Con bên trái không bị cắt bỏ, thay vào đó cây được phục hồi sau đó bằng cách cắt con bên phải mới được thêm vào lá ngoài cùng bên phải nhằm mục đích chuyển hướng. Xem bài đăng cập nhật của tôi với mã.
brainydexter

1
Bản phác thảo đẹp, nhưng tôi vẫn không hiểu điều kiện của vòng lặp while. Tại sao việc kiểm tra pre-> right! = Current lại cần thiết?
No_name

6
Tôi không hiểu tại sao điều này lại hiệu quả. Sau khi bạn in A, thì Y trở thành gốc, và bạn vẫn có A là con bên trái. Vì vậy, chúng tôi đang ở trong tình trạng giống như trước đây. Và chúng tôi lặp lại A. Trên thực tế, nó trông giống như một vòng lặp vô hạn.
user678392

Điều này không cắt đứt kết nối giữa Y và B sao? Khi X được đặt là hiện tại và Y được đặt là trước, thì nó sẽ nhìn xuống cây con bên phải của trước cho đến khi tìm thấy hiện tại (X), và sau đó đặt trước => phải là NULL, đó sẽ là B phải không? Theo mã được đăng ở trên
Đạt được

17

Các đệ quy trong trật tự traversal là: (in-order(left)->key->in-order(right)). (điều này tương tự như DFS)

Khi chúng tôi thực hiện DFS, chúng tôi cần biết nơi để quay lại (đó là lý do tại sao chúng tôi thường giữ một ngăn xếp).

Khi chúng ta đi qua một nút cha mà chúng ta sẽ cần quay lại -> chúng ta tìm thấy nút mà chúng ta sẽ cần quay lại từ đó và cập nhật liên kết của nó thành nút mẹ.

Khi nào chúng ta quay lại? Khi chúng ta không thể tiến xa hơn. Khi chúng ta không thể tiến xa hơn? Khi không còn quà của đứa trẻ.

Nơi chúng ta quay trở lại? Thông báo: đến THÀNH CÔNG!

Vì vậy, khi chúng ta đi theo các nút dọc theo đường con bên trái, hãy đặt nút tiền nhiệm ở mỗi bước để trỏ đến nút hiện tại. Bằng cách này, những người đi trước sẽ có liên kết đến những người kế nhiệm (một liên kết để bẻ khóa ngược).

Chúng tôi đi theo bên trái trong khi chúng tôi có thể cho đến khi chúng tôi cần quay lại. Khi chúng ta cần backtrack, chúng ta in nút hiện tại và theo liên kết bên phải đến nút kế nhiệm.

Nếu chúng ta vừa đánh dấu lùi -> chúng ta cần theo dõi con phải (với con trái là xong).

Làm thế nào để biết liệu chúng tôi có vừa bị lùi lại? Lấy tiền thân của nút hiện tại và kiểm tra xem nó có liên kết đúng (tới nút này) hay không. Nếu nó có - hơn chúng tôi đã theo dõi nó. gỡ bỏ liên kết để phục hồi cây.

Nếu không có link bên trái => chúng ta không backtrack và nên tiến hành theo các con bên trái.

Đây là mã Java của tôi (Xin lỗi, nó không phải là 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
Tôi thích câu trả lời của bạn rất nhiều vì nó cung cấp lý do cấp cao để đi đến giải pháp này!
KFL

6

Tôi đã tạo hoạt ảnh cho thuật toán tại đây: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

Điều này hy vọng sẽ giúp hiểu được. Vòng tròn màu xanh lam là con trỏ và mỗi trang chiếu là một lần lặp lại của vòng lặp while bên ngoài.

Đây là mã cho morris traversal (Tôi đã sao chép và sửa đổi nó từ những người yêu thích máy tính):

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)

Hoạt hình của bạn khá thú vị. Vui lòng xem xét việc biến nó thành một hình ảnh sẽ được đưa vào bài đăng của bạn, vì các liên kết bên ngoài thường chết sau một thời gian.
laancelot

1
Hình ảnh động rất hữu ích!
yyFred

bảng tính tuyệt vời và cách sử dụng thư viện binarytree. nhưng mã không đúng, nó không in được các nút gốc. bạn cần thêm print(cursor.value)sau pre.right = Nonedòng
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
            }        
        }
    }

Tôi nghĩ mã này sẽ dễ hiểu hơn, chỉ cần sử dụng null để tránh vòng lặp vô hạn, không cần phải sử dụng phép thuật khác. Nó có thể dễ dàng sửa đổi để đặt hàng trước.


1
Giải pháp là rất gọn gàng nhưng có một vấn đề. Theo Knuth, cuối cùng cây không nên bị sửa đổi. Bằng cách làm temp.left = nullcây sẽ bị mất.
Ankur

Phương pháp này có thể được sử dụng ở những nơi như chuyển đổi cây nhị phân thành danh sách liên kết.
cyber_raj

Giống như những gì @Shan đã nói, thuật toán không được thay đổi cây gốc. Trong khi thuật toán của bạn hoạt động để duyệt qua nó, nó sẽ phá hủy cây ban đầu. Do đó, điều này thực sự khác với thuật toán ban đầu và do đó gây hiểu lầm.
ChaoSXDemon

2

Tôi đã tìm thấy một lời giải thích bằng hình ảnh rất hay về Morris Traversal .

Morris Traversal


Câu trả lời chỉ liên kết sẽ mất giá trị khi liên kết bị hỏng trong tương lai, vui lòng thêm ngữ cảnh liên quan từ liên kết vào câu trả lời.
Arun Vinoth

Chắc chắn rồi. Tôi sẽ bổ sung nó sớm.
Ashish Ranjan

1

Tôi hy vọng mã giả bên dưới tiết lộ nhiều hơn:

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ập đến mã C ++ trong câu hỏi, vòng lặp while bên trong tìm thấy nút tiền nhiệm theo thứ tự của nút hiện tại. Trong một cây nhị phân tiêu chuẩn, con bên phải của phần tử tiền nhiệm phải là null, trong khi trong phiên bản luồng, con bên phải phải trỏ đến nút hiện tại. Nếu nút con bên phải là null, nó được đặt thành nút hiện tại, tạo hiệu quả luồng , được sử dụng như một điểm trả về mà nếu không thì phải được lưu trữ, thường là trên một ngăn xếp. Nếu cây con bên phải không rỗng, thì thuật toán đảm bảo rằng cây ban đầu được khôi phục và sau đó tiếp tục duyệt trong cây con bên phải (trong trường hợp này, người ta biết rằng cây con bên trái đã được truy cập).


0

Giải pháp Python Độ phức tạp về thời gian: O (n) Độ phức tạp về không gian: O (1)

Giải thích xuất sắc của Morris Inorder Traversal

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

Tôi xin lỗi, nhưng rất tiếc đây không phải là câu trả lời trực tiếp cho câu hỏi. OP đã yêu cầu giải thích về cách nó hoạt động chứ không phải cách triển khai, có thể vì họ muốn tự triển khai thuật toán. Nhận xét của bạn là tốt cho những người đã hiểu thuật toán, nhưng OP thì chưa. Ngoài ra, như một chính sách, các câu trả lời nên được khép kín thay vì chỉ liên kết với một số tài nguyên bên ngoài, bởi vì liên kết có thể thay đổi hoặc đứt gãy theo thời gian. Bạn có thể bao gồm các liên kết, nhưng nếu có, bạn cũng nên bao gồm ít nhất ý chính của những gì liên kết đang cung cấp.
Anonymous1847,

0

PFB Giải thích về Chuyển động theo thứ tự Morris.

  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;
        }
    }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.