解释不使用堆栈或递归的 Morris 中序树遍历
有人可以帮助我理解以下莫里斯中序树遍历算法而不使用堆栈或递归吗?我试图理解它是如何工作的,但它只是逃避了我。
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
我了解树的修改方式是将当前节点
作为右子树中
并使用此属性进行中序遍历。但除此之外,我迷失了。最大节点
的右子节点
编辑: 找到了附带的 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 */
}
Can someone please help me understand the following Morris inorder tree traversal algorithm without using stacks or recursion ? I was trying to understand how it works, but its just escaping me.
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
I understand the tree is modified in a way that the current node
, is made the right child
of the max node
in right subtree
and use this property for inorder traversal. But beyond that, I'm lost.
EDIT:
Found this accompanying c++ code. I was having a hard time to understand how the tree is restored after it is modified. The magic lies in else
clause, which is hit once the right leaf is modified. See code for 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 */
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
如果我正确地阅读了该算法,这应该是其工作原理的示例:
首先,
X
是根,因此它被初始化为current
。X
有一个左子树,因此X
成为X
左子树的最右边的右子树——的直接前身X
中序遍历。因此,X
成为B
的右子节点,然后current
设置为Y
。树现在看起来像这样:上面的
(Y)
指的是Y
及其所有子节点,由于递归问题而被省略。无论如何,重要的部分都列出来了。现在树有一个返回X的链接,遍历继续...
然后输出
A
,因为它没有左孩子,并且current
返回到Y
,在上一次迭代中成为A
的右子节点。在下一次迭代中,Y 拥有两个孩子。然而,循环的双重条件使其在到达自身时停止,这表明它的左子树已被遍历。因此,它打印自身,并继续其右子树,即B
。B
打印自身,然后current
变为X
,它也经历与Y
相同的检查过程意识到它的左子树已被遍历,继续Z
。树的其余部分遵循相同的模式。不需要递归,因为不是依赖于通过堆栈进行回溯,而是将返回到(子)树根的链接移动到无论如何都会在递归中序树遍历算法中访问它的点 - 在其结束之后左子树已经完成。
If I am reading the algorithm right, this should be an example of how it works:
First,
X
is the root, so it is initialized ascurrent
.X
has a left child, soX
is made the rightmost right child ofX
's left subtree -- the immediate predecessor toX
in an inorder traversal. SoX
is made the right child ofB
, thencurrent
is set toY
. The tree now looks like this:(Y)
above refers toY
and all of its children, which are omitted for recursion issues. The important part is listed anyway.Now that the tree has a link back to X, the traversal continues...
Then
A
is outputted, because it has no left child, andcurrent
is returned toY
, which was madeA
's right child in the previous iteration. On the next iteration, Y has both children. However, the dual-condition of the loop makes it stop when it reaches itself, which is an indication that it's left subtree has already been traversed. So, it prints itself, and continues with its right subtree, which isB
.B
prints itself, and thencurrent
becomesX
, which goes through the same checking process asY
did, also realizing that its left subtree has been traversed, continuing with theZ
. The rest of the tree follows the same pattern.No recursion is necessary, because instead of relying on backtracking through a stack, a link back to the root of the (sub)tree is moved to the point at which it would be accessed in a recursive inorder tree traversal algorithm anyway -- after its left subtree has finished.
递归中序遍历为:
(in-order(left)->key->in-order(right))
。 (这与DFS类似)当我们进行DFS时,我们需要知道回溯到哪里(这就是我们通常保留堆栈的原因)。
当我们遍历父节点时,我们需要回溯到 ->我们找到需要回溯的节点并将其链接更新到父节点。
当我们回溯时?当我们无法走得更远的时候。什么时候我们不能走得更远?当没有留下孩子的礼物时。
我们回溯到哪里?注意:致继承人!
因此,当我们沿着左子路径跟踪节点时,将每一步的前驱节点设置为指向当前节点。这样,前任就会有到后继者的链接(回溯的链接)。
我们尽可能向左行驶,直到需要原路返回。当我们需要回溯时,我们打印当前节点并沿着正确的链接到达后继节点。
如果我们刚刚回溯->我们需要跟随右孩子(我们已经完成了左孩子)。
如何判断我们是否刚刚回溯呢?获取当前节点的前驱节点并检查它是否有正确的链接(到该节点)。如果有——那么我们就遵循它。删除链接以恢复树。
如果没有左侧链接=>我们没有走回头路,应该继续追随剩下的孩子。
这是我的 Java 代码(抱歉,它不是 C++)
The recursive in-order traversal is :
(in-order(left)->key->in-order(right))
. (this is similar to DFS)When we do the DFS, we need to know where to backtrack to (that's why we normally keep a stack).
As we go through a parent node to which we will need to backtrack to -> we find the node which we will need to backtrack from and update its link to the parent node.
When we backtrack? When we cannot go further. When we cannot go further? When no left child's present.
Where we backtrack to? Notice: to SUCCESSOR!
So, as we follow nodes along left-child path, set the predecessor at each step to point to the current node. This way, the predecessors will have links to successors (a link for backtracking).
We follow left while we can until we need to backtrack. When we need to backtrack, we print the current node and follow the right link to the successor.
If we have just backtracked -> we need to follow the right child (we are done with left child).
How to tell whether we have just backtracked? Get the predecessor of the current node and check if it has a right link (to this node). If it has - than we followed it. remove the link to restore the tree.
If there was no left link => we did not backtrack and should proceed following left children.
Here's my Java code (Sorry, it is not C++)
我在这里为该算法制作了一个动画:
https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa -WR0VsPU38fg/edit?usp=sharing
这应该有助于理解。蓝色圆圈是光标,每张幻灯片都是外部 while 循环的迭代。
这是morris遍历的代码(我从极客那里复制并修改为极客):
I've made an animation for the algorithm here:
https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing
This should hopefully help to understand. The blue circle is the cursor and each slide is an iteration of the outer while loop.
Here's code for morris traversal (I copied and modified it from geeks for geeks):
我找到了莫里斯遍历的很好的图解。
I found a very good pictorial explanation of Morris Traversal.
我希望下面的伪代码更具启发性:
参考问题中的 C++ 代码,内部 while 循环找到当前节点的有序前驱。在标准二叉树中,前驱的右子节点必须为空,而在线程版本中,右子节点必须指向当前节点。如果右子节点为空,则将其设置为当前节点,从而有效地创建线程,其中用作返回点,否则必须存储在堆栈上。如果右子树不为空,则算法确保恢复原始树,然后继续遍历右子树(在这种情况下,已知左子树已被访问)。
I hope the pseudo-code below is more revealing:
Referring to the C++ code in the question, the inner while loop finds the in-order predecessor of the current node. In a standard binary tree, the right child of the predecessor must be null, while in the threaded version the right child must point to the current node. If the right child is null, it is set to the current node, effectively creating the threading, which is used as a returning point that would otherwise have to be on stored, usually on a stack. If the right child is not null, then the algorithm makes sure that the original tree is restored, and then continues traversal in the right subtree (in this case it is known that the left subtree was visited).
我认为这段代码会更好理解,只需使用 null 来避免无限循环,不必使用其他魔法。它可以轻松修改为预购。
I think this code would be better to understand, just use a null to avoid infinite loops, don't have to use magic else. It can be easily modified to preorder.
Python解决方案
时间复杂度:O(n)
空间复杂度:O(1)
优秀的莫里斯中序遍历解释
Python Solution
Time Complexity : O(n)
Space Complexity : O(1)
Excellent Morris Inorder Traversal Explanation
Morris 中序遍历的 PFB 解释。
PFB Explanation of Morris In-order Traversal.