【问题标题】:Explain Morris inorder tree traversal without using stacks or recursion在不使用堆栈或递归的情况下解释 Morris 中序树遍历
【发布时间】:2011-03-31 16:11:16
【问题描述】:

有人可以帮我理解以下不使用堆栈或递归的莫里斯中序树遍历算法吗?我试图了解它是如何工作的,但它只是逃避了我。

 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

我了解树的修改方式为将current node 设为max node 中的right childright 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 */
}

【问题讨论】:

  • 我以前从未听说过这种算法。相当优雅!
  • 我认为指出the source of the pseudo-code + code 可能有用(大概)。
  • 在上面的代码中,下面这行不是必须的:pre->right = NULL;
  • 我认为伪代码有一个重要的遗漏错误。在“Else”的步骤“a”中,它表示标记current 的前任并向左移动。这应该说类似“如果我们在找到它自己的前任时遇到current,则取消线程(可选,如果您不想让树保持线程)并向右移动”。我认为这就是@Talonj 在他们出色的答案中所说的“循环的双重条件”的意思。这里的教训是代码比描述更重要。

标签: c++ binary-tree tree-traversal


【解决方案1】:

如果我没看错算法,这应该是它工作原理的一个例子:

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

首先X是根,所以初始化为currentX 有一个左孩子,因此X 成为X 左子树的最右孩子——在中序遍历中X 的直接前身。所以XB 的右孩子,然后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。树的其余部分遵循相同的模式。

不需要递归,因为不是依赖于通过堆栈回溯,而是将返回(子)树根的链接移动到递归中序树遍历算法中将访问它的点 - - 在它的左子树完成之后。

【讨论】:

  • 感谢您的解释。左孩子没有被切断,而是稍后通过切断新的右孩子来恢复树,该右孩子被添加到最右边的叶子以进行遍历。请参阅我更新后的代码。
  • 不错的草图,但我仍然不明白 while 循环条件。为什么需要检查 pre->right != current?
  • 我不明白为什么会这样。打印 A 后,Y 成为根,并且您仍然有 A 作为左孩子。因此,我们处于与以前相同的情况。然后我们重复A。实际上,它看起来像一个无限循环。
  • 这不是切断Y和B之间的联系吗?当X设置为current,Y设置为pre时,它会向下查找pre的右子树,直到找到current(X),然后设置pre=>right为NULL,这将是B对吗?按照上面贴的代码
  • 感谢您的简单解释。
【解决方案2】:

递归中序遍历是:(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;
}

【讨论】:

  • 我非常喜欢您的回答,因为它为提出此解决方案提供了高级推理!
  • 急需一张图表
【解决方案3】:

我在这里为算法制作了动画: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

这应该有助于理解。蓝色圆圈是光标,每张幻灯片都是外部 while 循环的迭代。

这是 morris 遍历的代码(我从 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)

【讨论】:

  • 你的动画很有趣。请考虑将其制作成可以包含在您的帖子中的图片,因为外部链接通常会在一段时间后失效。
  • 动画很有帮助!
  • 很好的电子表格和二叉树库的使用。但代码不正确,无法打印根节点。您需要在pre.right = None 行之后添加print(cursor.value)
【解决方案4】:

我找到了一个很好的Morris Traversal的图文解说。

【讨论】:

  • 未来链接断开时,仅链接的答案将失去其价值,请将链接中的相关上下文添加到答案中。
  • 当然。我会尽快添加。
【解决方案5】:
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 以避免无限循环,不必使用魔法 else。它可以很容易地修改为预购。

【讨论】:

  • 解决方案非常简洁,但有一个问题。根据 Knuth 的说法,树最终不应该被修改。通过执行temp.left = null 树将丢失。
  • 这个方法可以用在二叉树转链表等地方。
  • 就像@Shan 所说的那样,算法不应该改变原始树。虽然您的算法适用于遍历它,但它会破坏原始树。因此,这实际上与原始算法不同,因此具有误导性。
【解决方案6】:

我希望下面的伪代码更能说明问题:

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循环查找当前节点的有序前驱。在标准二叉树中,前任的右孩子必须为空,而在线程版本中,右孩子必须指向当前节点。如果右子节点为空,则将其设置为当前节点,从而有效地创建threading,它用作返回点,否则必须存储,通常在堆栈上。如果右子树为空,则算法确保恢复原始树,然后继续遍历右子树(在这种情况下,知道访问了左子树)。

【讨论】:

    【解决方案7】:

    Python 解决方案 时间复杂度:O(n) 空间复杂度:O(1)

    Excellent 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
    

    【讨论】:

    • 很抱歉,但很遗憾,这不是问题的直接答案。 OP 要求解释它是如何工作的,而不是实现,可能是因为他们想自己实现算法。您的 cmets 非常适合已经了解算法但 OP 还没有的人。此外,作为一项政策,答案应该是独立的,而不是仅仅链接到一些外部资源,因为链接可能会随着时间的推移而改变或中断。可以包含链接,但如果包含链接,您还应该至少包含链接所提供内容的要点。
    【解决方案8】:

    Morris中序遍历的PFB解释。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-02
      • 2012-05-09
      • 1970-01-01
      • 2023-03-19
      • 2011-03-13
      • 1970-01-01
      相关资源
      最近更新 更多