Объясните обход дерева порядка Морриса без использования стеков или рекурсии

126

Может ли кто-нибудь помочь мне понять следующий алгоритм обхода дерева порядка Морриса без использования стеков или рекурсии? Я пытался понять, как это работает, но это просто ускользало от меня.

 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

Я понимаю , что дерево модифицируется таким образом , что current node, сделано right childиз max nodeв right subtreeи использовать это свойство для заказовМои обходе. Но кроме этого, я потерялся.

РЕДАКТИРОВАТЬ: нашел этот сопровождающий код С ++. Мне было трудно понять, как дерево восстанавливается после модификации. Магия заключается в 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 */
}
brainydexter
источник
13
Я никогда раньше не слышал об этом алгоритме. Довольно элегантно!
Fred Foo
5
Я подумал, что может быть полезно указать источник псевдокода + кода (предположительно).
Бернхард Баркер
1
Источник: geeksforgeeks.org/...
DebashisDeb
в приведенном выше коде следующую строку не требуется: pre->right = NULL;
prashant.kr.mod

Ответы:

155

Если я правильно понимаю алгоритм, это должен быть пример того, как он работает:

     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. Остальная часть дерева следует той же схеме.

Никакой рекурсии не требуется, потому что вместо того, чтобы полагаться на возврат через стек, обратная ссылка на корень (под) дерева перемещается в точку, в которой она будет доступна в рекурсивном алгоритме обхода дерева порядка в любом случае - после его левое поддерево закончено.

Talonj
источник
3
Спасибо за объяснение. Левый дочерний элемент не отделяется, вместо этого дерево позже восстанавливается путем разделения нового правого дочернего элемента, который добавляется к самому правому листу с целью обхода. Смотрите мой обновленный пост с кодом.
brainydexter
1
Хороший набросок, но я до сих пор не понимаю условия цикла while. Почему необходима проверка наличия pre-> right! = Current?
No_name
6
Я не понимаю, почему это работает. После того, как вы напечатаете A, Y станет корнем, и у вас останется A в качестве левого потомка. Таким образом, мы находимся в той же ситуации, что и раньше. И повторяем А. На самом деле это похоже на бесконечный цикл.
user678392
Разве это не разрывает связь между Y и B? Когда X установлен как current, а Y установлен как pre, тогда он будет смотреть вниз по правому поддереву pre, пока не найдет current (X), а затем он устанавливает pre => right как NULL, что было бы B правильно? В соответствии с приведенным выше кодом
Ахинт 01
17

Рекурсивный обход в-заказ: (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;
}
Мария Сахарова
источник
4
Мне очень нравится ваш ответ, потому что он дает высокоуровневые аргументы в пользу этого решения!
KFL
6

Я сделал анимацию для алгоритма здесь: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

Надеюсь, это поможет понять. Синий кружок - это курсор, а каждый слайд - это итерация внешнего цикла while.

Вот код для обхода Морриса (я скопировал и модифицировал его у гиков для гиков):

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)
Райан Бургерт
источник
Ваша анимация довольно интересная. Пожалуйста, подумайте о том, чтобы превратить его в изображение, которое будет включено в ваш пост, поскольку внешние ссылки часто умирают через некоторое время.
laancelot 01
1
Анимация полезна!
yyFred
отличная электронная таблица и использование библиотеки binarytree. но код неправильный, он не может распечатать корневые узлы. вам нужно добавить print(cursor.value)после pre.right = Noneстроки
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
            }        
        }
    }

Я думаю, что этот код было бы лучше понять, просто используйте null, чтобы избежать бесконечных циклов, не нужно использовать магию еще. Его можно легко изменить на предзаказ.

Adeath
источник
1
Решение очень хорошее, но есть одна проблема. По словам Кнута, дерево в конечном итоге не должно изменяться. При этом temp.left = nullдерево будет потеряно.
Ankur
Этот метод можно использовать в таких случаях, как преобразование двоичного дерева в связанный список.
cyber_raj 09
Как и то, что сказал @Shan, алгоритм не должен изменять исходное дерево. Хотя ваш алгоритм работает для его обхода, он разрушает исходное дерево. Следовательно, этот алгоритм на самом деле отличается от исходного и, следовательно, вводит в заблуждение.
ChaoSXDemon
2

Я нашел очень хорошее иллюстрированное объяснение Морриса Траверсала .

Моррис Трэверсал

Ашиш Ранджан
источник
Ответ только по ссылке потеряет свою ценность, если в будущем ссылка будет повреждена, пожалуйста, добавьте соответствующий контекст из ссылки в ответ.
Арун Винот,
Конечно. Скоро добавлю.
Ашиш Ранджан,
1

Я надеюсь, что приведенный ниже псевдокод более показателен:

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, он устанавливается на текущий узел, эффективно создавая потоки , которые используются в качестве точки возврата, которая в противном случае должна была бы храниться, обычно в стеке. Если правый дочерний элемент не равен нулю, то алгоритм гарантирует, что исходное дерево восстановлено, а затем продолжает обход в правом поддереве (в этом случае известно, что левое поддерево было посещено).

EXP
источник
0

Сложность времени решения Python: O (n) Сложность пространства: O (1)

Отличное объяснение обхода Морриса Inorder

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 попросил объяснить, как это работает, а не реализацию, возможно потому, что они хотят реализовать алгоритм самостоятельно. Ваши комментарии хороши для тех, кто уже понимает алгоритм, но OP еще не понимает. Кроме того, как правило, ответы должны быть автономными, а не просто ссылаться на какой-либо внешний ресурс, потому что ссылка может измениться или сломаться со временем. Можно включать ссылки, но если вы это сделаете, вы также должны включить хотя бы суть того, что предоставляет ссылка.
Anonymous1847,
0

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;
        }
    }
Дишант Батра
источник