Поясніть обхід дерева дерев Морріса без використання стеків та рекурсії


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і використовувати цю властивість для заказовМоі обході. Але поза цим я загублений.

EDIT: Знайдено цей супровідний код 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 */
}

12
Я ніколи раніше не чув про цей алгоритм. Досить елегантно!
Фред Фоо

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. Решта дерева слідує за тією ж схемою.

Рекурсія не потрібна, тому що замість того, щоб покладатися на зворотний трек через стек, посилання назад до кореня (під) дерева переміщується до тієї точки, в якій би в будь-якому випадку можна було отримати доступ до алгоритму траверсу рекурсивного дерева завдань - після його ліве піддерево закінчилося.


3
Дякую за пояснення. Ліва дитина не відрізана, натомість дерево відновлюється пізніше відсіканням нової правої дитини, яка додається до крайнього правого листка з метою обходу. Дивіться мій оновлений пост з кодом.
brainydexter

1
Хороший ескіз, але я досі не розумію умови циклу. Чому потрібна перевірка на пред-> правильно! = Струм?
No_name

6
Я не бачу, чому це працює. Після того, як ви надрукуєте A, тоді Y стає коренем, і ви все одно маєте A як ліворуч. Таким чином, ми перебуваємо в тій же ситуації, що і раніше. І ми повторюємо А. Насправді це схоже на нескінченну петлю.
користувач678392

Чи це не перериває зв’язок між Y і B? Коли X встановлено як поточний, а Y встановлено як попередній, тоді він буде шукати нижнє право піддерева pre, поки не знайде струм (X), а потім він встановить pre => право як NULL, що було б B правильно? Відповідно до коду, розміщеного вище
Achint

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

Це, сподіваємось, допоможе зрозуміти. Синє коло - це курсор, і кожен слайд - це ітерація зовнішньої петлі.

Ось код для обходу моріса (я скопіював і змінив його з вундиків для вундуків):

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

1
Анімація корисна!
yyFred

велика електронна таблиця та використання бібліотеки бінарних дерев. але код невірний, він не може надрукувати кореневі вузли. вам потрібно додати 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
            }        
        }
    }

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


1
Рішення дуже охайне, але є одна проблема. За словами Кнут, дерево не повинно бути змінене зрештою. Тим самим temp.left = nullдерево загубиться.
Анкур

Цей метод можна використовувати в таких місцях, як перетворення бінарного дерева у пов'язаний список.
cyber_raj

Як і те, що сказав @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 знаходить порядок попередника поточного вузла. У стандартному двійковому дереві право дочірнього попередника має бути нульовим, тоді як у потоковому варіанті правильна дитина повинна вказувати на поточний вузол. Якщо потрібна дитина дорівнює нулю, вона встановлюється на поточний вузол, фактично створюючи нитку , яка використовується як точка повернення, яка в іншому випадку повинна бути збережена, як правило, в стеці. Якщо права дитина не є нульовою, алгоритм гарантує, що початкове дерево відновлено, а потім продовжує обхід у правому піддереві (у цьому випадку відомо, що ліве піддерево було відвідано).


0

Час складності рішення Python: O (n) Складність простору: O (1)

Відмінне роз'яснення міжрядкового переходу Morris

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

Вибачте, але це, на жаль, не є прямою відповіддю на питання. ОП попросила пояснення, як воно працює, а не реалізацію, можливо, тому, що вони хочуть реалізувати алгоритм самостійно. Ваші коментарі хороші для того, хто вже розуміє алгоритм, але ОП ще не має. Також, як політика, відповіді повинні бути самостійними, а не просто посилатися на якийсь зовнішній ресурс, оскільки посилання може змінитися або розірватися з часом. Добре включати посилання, але якщо ви це зробите, ви також повинні включити принаймні суть того, що надає посилання.
Анонім1818

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;
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.