Explique a travessia da árvore inorder de Morris sem usar pilhas ou recursão


125

Alguém pode me ajudar a entender o seguinte algoritmo de passagem de árvore inorder de Morris sem usar pilhas ou recursão? Eu estava tentando entender como funciona, mas está apenas me escapando.

 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

Eu entendo que a árvore é modificada de forma que o current node, é feito o right childde max nodeem right subtreee usa essa propriedade para travessia inorder. Mas, além disso, estou perdido.

EDIT: Encontrado este código c ++ acompanhante. Eu estava tendo dificuldade em entender como a árvore é restaurada depois de modificada. A mágica está na elsecláusula, que é atingida assim que a folha certa é modificada. Veja o código para obter detalhes:

/* 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
Eu nunca tinha ouvido falar desse algoritmo antes. Muito elegante!
Fred Foo

5
Achei que seria útil indicar a fonte do pseudocódigo + código (presumivelmente).
Bernhard Barker


no código acima, a seguinte linha não é necessária: pre->right = NULL;
prashant.kr.mod

Respostas:


155

Se estou lendo o algoritmo corretamente, este deve ser um exemplo de como funciona:

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

Primeiro, Xé a raiz, portanto, é inicializado como current. Xtem um filho à esquerda, então Xé tornado o filho mais à direita da Xsubárvore esquerda de - o predecessor imediato de Xem uma travessia inorder. Então Xé feita a criança direita B, então currentestá definido para Y. A árvore agora se parece com isto:

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

(Y)acima se refere a Ye todos os seus filhos, que são omitidos por problemas de recursão. A parte importante é listada de qualquer maneira. Agora que a árvore tem um link para o X, a travessia continua ...

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

Em seguida, Aé emitido, porque não tem filho esquerdo, e currenté retornado para Y, que foi tornado Afilho direito na iteração anterior. Na próxima iteração, Y tem os dois filhos. No entanto, a condição dupla do loop o faz parar quando atinge a si mesmo, o que é uma indicação de que sua subárvore esquerda já foi percorrida. Então, ele se imprime e continua com sua subárvore certa, que é B.

Bimprime a si mesmo, e então currentse torna X, que passa pelo mesmo processo de verificação que Yfez, também percebendo que sua subárvore esquerda foi percorrida, continuando com o Z. O resto da árvore segue o mesmo padrão.

Nenhuma recursão é necessária, porque em vez de depender do retrocesso através de uma pilha, um link de volta à raiz da (sub) árvore é movido para o ponto em que seria acessado em um algoritmo de passagem de árvore de ordem recursiva de qualquer maneira - após seu a subárvore esquerda terminou.


3
Obrigada pelo esclarecimento. O filho da esquerda não é cortado; em vez disso, a árvore é restaurada posteriormente cortando o novo filho da direita que é adicionado à folha mais à direita para fins de travessia. Veja minha postagem atualizada com o código.
brainydexter

1
Belo esboço, mas ainda não entendo a condição do loop while. Por que a verificação de pre-> right! = Current é necessária?
No_name

6
Não vejo por que isso funciona. Depois de imprimir A, Y se torna a raiz e você ainda tem A como filho esquerdo. Assim, estamos na mesma situação de antes. E repetimos A. Na verdade, parece um loop infinito.
user678392

Isso não corta a conexão entre Y e B? Quando X é definido como atual e Y é definido como pré, então ele vai procurar na subárvore direita de pré até encontrar current (X), e então definir pre => certo como NULL, o que seria B certo? De acordo com o código postado acima
Achint

17

A travessia de-ordem em recursiva é: (in-order(left)->key->in-order(right)). (é semelhante ao DFS)

Quando fazemos o DFS, precisamos saber para onde voltar (é por isso que normalmente mantemos uma pilha).

À medida que passamos por um nó pai para o qual precisaremos retroceder para -> encontramos o nó do qual precisaremos retroceder e atualizar seu link para o nó pai.

Quando voltarmos? Quando não podemos ir mais longe. Quando não podemos ir mais longe? Quando nenhuma criança esquerda está presente.

Para onde voltamos? Aviso: ao SUCESSOR!

Assim, conforme seguimos os nós ao longo do caminho filho esquerdo, defina o predecessor em cada etapa para apontar para o nó atual. Dessa forma, os predecessores terão links para sucessores (um link para retrocesso).

Seguimos para a esquerda enquanto podemos até que precisamos voltar atrás. Quando precisamos voltar, imprimimos o nó atual e seguimos o link correto para o sucessor.

Se apenas recuamos -> precisamos seguir a criança certa (terminamos com a criança esquerda).

Como saber se acabamos de voltar? Obtenha o predecessor do nó atual e verifique se ele possui um link correto (para este nó). Se foi - então nós o seguimos. remova o link para restaurar a árvore.

Se não houvesse link para a esquerda => não retrocedemos e devemos seguir seguindo os filhos esquerdos.

Este é meu código Java (desculpe, não é 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
Gosto muito da sua resposta porque fornece um raciocínio de alto nível para chegar a essa solução!
KFL de

6

Fiz uma animação para o algoritmo aqui: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

Espero que isso ajude a entender. O círculo azul é o cursor e cada slide é uma iteração do loop while externo.

Aqui está o código para morris traversal (eu copiei e modifiquei de geeks para 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)

Sua animação é bem interessante. Por favor, considere transformá-la em uma imagem que seria incluída em sua postagem, já que links externos geralmente morrem depois de algum tempo.
laancelot de

1
A animação é útil!
yyFred

ótima planilha e uso da biblioteca binarytree. mas o código não está correto, ele não consegue imprimir os nós raiz. você precisa adicionar print(cursor.value)após a pre.right = Nonelinha
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
            }        
        }
    }

Acho que seria melhor entender esse código, basta usar um nulo para evitar loops infinitos, não precisa usar mais magia. Ele pode ser facilmente modificado para pré-encomenda.


1
A solução é muito simples, mas há um problema. De acordo com Knuth, a árvore não deve ser modificada no final. Fazendo temp.left = nullárvore será perdida.
Ankur

Este método pode ser usado em lugares como a conversão de uma árvore binária em uma lista vinculada.
cyber_raj

Assim como @Shan disse, o algoritmo não deve alterar a árvore original. Embora seu algoritmo trabalhe para percorrê-lo, ele destrói a árvore original. Portanto, isso é realmente diferente do algoritmo original e, portanto, enganoso.
ChaoSXDemon


1

Espero que o pseudo-código abaixo seja mais revelador:

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

Referindo-se ao código C ++ na questão, o loop while interno encontra o predecessor em ordem do nó atual. Em uma árvore binária padrão, o filho certo do predecessor deve ser nulo, enquanto na versão encadeada o filho certo deve apontar para o nó atual. Se o filho certo for nulo, ele é definido como o nó atual, criando efetivamente o threading , que é usado como um ponto de retorno que, de outra forma, teria que estar armazenado, geralmente em uma pilha. Se o filho certo não for nulo, o algoritmo garante que a árvore original seja restaurada e, a seguir, continua o percurso na subárvore direita (neste caso, sabe-se que a subárvore esquerda foi visitada).


0

Complexidade do tempo da solução Python: O (n) Complexidade do espaço: O (1)

Excelente explicação do 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

Lamento, mas infelizmente esta não é uma resposta direta à pergunta. O OP pediu uma explicação de como ele funciona, não uma implementação, possivelmente porque eles próprios desejam implementar o algoritmo. Seus comentários são bons para quem já entende o algoritmo, mas o OP ainda não. Além disso, como política, as respostas devem ser autocontidas, em vez de meramente vincular a algum recurso externo, porque o link pode mudar ou quebrar com o tempo. Não há problema em incluir links, mas se fizer isso, você também deve incluir pelo menos a essência do que o link está fornecendo.
Anônimo

0

Explicação PFB de Morris In-Order Traversal.

  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;
        }
    }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.