web-dev-qa-db-ja.com

スタックまたは再帰を使用せずにMorrisの順序ツリートラバーサルを説明する

スタックや再帰を使用せずに、次の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 noderight child内のmax noderight 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 */
}
94
brainydexter

アルゴリズムを正しく読んでいる場合、これはどのように機能するかの例です。

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

まず、Xがルートであるため、currentとして初期化されます。 Xには左の子があるため、Xは、Xの左のサブツリーの右端の子になります-順序トラバーサルのXの直前の子。したがって、XBの正しい子になり、currentYに設定されます。ツリーは次のようになります。

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

(Y)は、Yとそのすべての子を指します。これらは再帰の問題のために省略されています。とにかく重要な部分がリストされています。ツリーがXに戻るリンクを持っているので、トラバーサルが続きます...

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

その後、Aが出力されます。これは、左の子がないためです。また、currentYに返されます。これは、前の反復でAの右の子になりました。次の反復で、Yには両方の子があります。ただし、ループの二重条件により、ループ自体に達すると停止します。これは、左のサブツリーがすでに通過したことを示しています。そのため、それ自体を出力し、その右のサブツリーであるBを継続します。

Bはそれ自体を出力し、currentXになります。これはYと同じチェックプロセスを経て、左のサブツリーが走査され、Zを続けていることを認識します。ツリーの残りの部分も同じパターンに従います。

スタックを介したバックトラッキングに依存する代わりに、(サブ)ツリーのルートへのリンクが再帰的順序ツリートラバーサルアルゴリズムでアクセスされるポイントに移動されるため、再帰は必要ありません。左のサブツリーが終了しました。

135
Talonj

再帰的順序走査は次のとおりです:(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;
}
9
Maria Sakharova
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を使用するだけで、他のマジックを使用する必要はありません。事前注文に簡単に変更できます。

3
Adeath

以下の擬似コードがもっと明らかになることを願っています:

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の場合、アルゴリズムは元のツリーが復元されていることを確認し、右側のサブツリーでトラバースを続行します(この場合左のサブツリーにアクセスしたことがわかっています)。

1
EXP