スタックや再帰を使用せずに、次の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
current node
がright child
内のmax node
の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 */
}
アルゴリズムを正しく読んでいる場合、これはどのように機能するかの例です。
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
を続けていることを認識します。ツリーの残りの部分も同じパターンに従います。
スタックを介したバックトラッキングに依存する代わりに、(サブ)ツリーのルートへのリンクが再帰的順序ツリートラバーサルアルゴリズムでアクセスされるポイントに移動されるため、再帰は必要ありません。左のサブツリーが終了しました。
再帰的順序走査は次のとおりです:(in-order(left)->key->in-order(right))
。 (これはDFSに似ています)
DFSを実行するとき、どこにバックトラックするかを知る必要があります(そのため、通常はスタックを保持します)。
バックトラックする必要がある親ノードを通過するとき->バックトラックする必要があるノードを見つけ、親ノードへのリンクを更新します。
バックトラックするときは?さらに進むことができないとき。さらに進むことができないときは?残っている子供がいないとき。
どこに戻るのですか?通知:後継者に!
したがって、左の子パスに沿ってノードをたどるとき、各ステップで先行ノードを設定して現在のノードを指すようにします。このようにして、前任者は後継者へのリンク(バックトラッキングのリンク)を持ちます。
バックトラックする必要があるまで、できる限り左に進みます。バックトラックする必要がある場合は、現在のノードを出力し、後継者への正しいリンクをたどります。
バックトラックしたばかりの場合は、右の子を追跡する必要があります(左の子については完了です)。
バックトラックしたかどうかを見分ける方法は?現在のノードの先行ノードを取得し、(このノードへの)正しいリンクがあるかどうかを確認します。それがある場合-私たちはそれに従ったよりも。リンクを削除してツリーを復元します。
左側のリンクがなかった場合=>バックトラックせず、左側の子を追跡する必要があります。
ここに私の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;
}
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を使用するだけで、他のマジックを使用する必要はありません。事前注文に簡単に変更できます。
以下の擬似コードがもっと明らかになることを願っています:
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ループは現在のノードの順序どおりの先行を見つけます。標準のバイナリツリーでは、先行バージョンの右側の子はnullである必要がありますが、スレッドバージョンでは、右側の子は現在のノードを指している必要があります。右側の子がnullである場合、現在のノードに設定され、効果的に threading を作成します。これは、通常はスタック上に格納する必要がある戻りポイントとして使用されます。右側の子がnotnullの場合、アルゴリズムは元のツリーが復元されていることを確認し、右側のサブツリーでトラバースを続行します(この場合左のサブツリーにアクセスしたことがわかっています)。