Jelaskan penjelajahan pohon inorder Morris tanpa menggunakan stack atau rekursi


126

Adakah yang bisa membantu saya memahami algoritme penelusuran pohon inorder Morris berikut tanpa menggunakan tumpukan atau rekursi? Saya mencoba memahami cara kerjanya, tetapi itu hanya luput dari saya.

 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

Saya memahami pohon dimodifikasi sedemikian rupa sehingga current node, dibuat right childdari max nodedalam right subtreedan menggunakan properti ini untuk traversal inorder. Tapi di luar itu, saya tersesat.

EDIT: Menemukan kode c ++ yang menyertai ini. Saya mengalami kesulitan untuk memahami bagaimana pohon dipulihkan setelah dimodifikasi. Keajaiban terletak pada elseklausa, yang dipukul setelah daun kanan dimodifikasi. Lihat kode untuk detailnya:

/* 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
Saya belum pernah mendengar tentang algoritma ini sebelumnya. Sangat elegan!
Fred Foo

5
Saya pikir mungkin berguna untuk menunjukkan sumber pseudo-code + code (mungkin).
Bernhard Barker


dalam kode di atas, baris berikut tidak diperlukan: pre->right = NULL;
prashant.kr.mod

Jawaban:


155

Jika saya membaca algoritme dengan benar, ini seharusnya menjadi contoh cara kerjanya:

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

Pertama, Xadalah root, sehingga diinisialisasi sebagai current. Xmemiliki anak kiri, sehingga Xdibuat anak paling kanan dari Xsubpohon kiri - pendahulu langsung Xdalam inorder traversal. Jadi Xdibuatkan anak yang tepat B, lalu currentditetapkan ke Y. Pohon itu sekarang terlihat seperti ini:

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

(Y)di atas mengacu pada Ydan semua anaknya, yang dihilangkan karena masalah rekursi. Bagian yang penting tetap terdaftar. Sekarang pohon memiliki link kembali ke X, traversal berlanjut ...

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

Kemudian Adikeluarkan, karena tidak memiliki turunan kiri, dan currentdikembalikan ke Y, yang merupakan turunan Akanan pada iterasi sebelumnya. Pada iterasi berikutnya, Y memiliki kedua anak. Namun, kondisi ganda dari loop membuatnya berhenti ketika mencapai dirinya sendiri, yang merupakan indikasi bahwa subpohon kiri telah dilintasi. Jadi, ia mencetak dirinya sendiri, dan berlanjut dengan subtree kanannya, yaitu B.

Bmencetak dirinya sendiri, dan kemudian currentmenjadi X, yang menjalani proses pemeriksaan yang sama seperti Ysebelumnya, juga menyadari bahwa subpohon kirinya telah dilintasi, melanjutkan dengan Z. Sisa pohon lainnya mengikuti pola yang sama.

Tidak diperlukan rekursi, karena alih-alih mengandalkan penelusuran kembali melalui tumpukan, tautan kembali ke akar pohon (sub) dipindahkan ke titik di mana ia akan diakses dalam algoritme penjelajahan pohon inorder rekursif - setelah itu subtree kiri telah selesai.


3
Terima kasih untuk penjelasannya. Anak kiri tidak dipotong, sebagai gantinya pohon tersebut dipulihkan nanti dengan memutuskan anak kanan baru yang ditambahkan ke daun paling kanan untuk tujuan traversal. Lihat posting saya yang diperbarui dengan kode.
brainydexter

1
Sketsa yang bagus, tapi saya masih belum mengerti kondisi while loop. Mengapa memeriksa pra-> kanan! = Arus diperlukan?
No_name

6
Saya tidak mengerti mengapa ini berhasil. Setelah Anda mencetak A, kemudian Y menjadi root, dan Anda masih memiliki A sebagai anak kiri. Jadi, kami berada dalam situasi yang sama seperti sebelumnya. Dan kami ulangi A. Faktanya, ini terlihat seperti loop tak terbatas.
pengguna678392

Bukankah ini memutuskan hubungan antara Y dan B? Ketika X diatur sebagai arus dan Y diatur sebagai pra, maka itu akan melihat ke bawah subpohon kanan dari pra sampai menemukan arus (X), dan kemudian menetapkan pre => kanan sebagai NULL, yang akan menjadi B kan? Sesuai dengan kode yang diposting di atas
Achint

17

Rekursif di-order traversal adalah: (in-order(left)->key->in-order(right)). (ini mirip dengan DFS)

Ketika kita melakukan DFS, kita perlu tahu ke mana harus mundur (itulah mengapa kita biasanya menyimpan tumpukan).

Saat kita pergi melalui simpul induk yang kita perlu lacak kembali -> kita menemukan simpul yang kita perlu lacak dan memperbarui tautannya ke simpul induk.

Saat kita mundur? Ketika kita tidak bisa melangkah lebih jauh. Kapan kita tidak bisa melangkah lebih jauh? Saat tidak ada anak kiri yang hadir.

Kemana kita mundur? Pemberitahuan: kepada SUCCESSOR!

Jadi, saat kita mengikuti simpul di sepanjang jalur anak-kiri, atur pendahulu di setiap langkah untuk menunjuk ke simpul saat ini. Dengan cara ini, pendahulu akan memiliki tautan ke penerus (tautan untuk mundur).

Kami mengikuti kiri selagi kami bisa sampai kami perlu mundur. Ketika kita perlu mundur, kita mencetak simpul saat ini dan mengikuti tautan yang benar ke penggantinya.

Jika kita baru saja mundur -> kita harus mengikuti anak kanan (kita selesai dengan anak kiri).

Bagaimana cara mengetahui apakah kita baru saja mundur? Dapatkan pendahulu dari node saat ini dan periksa apakah memiliki link yang benar (ke node ini). Jika memiliki - daripada kita mengikutinya. hapus tautan untuk memulihkan pohon.

Jika tidak ada tautan kiri => kami tidak mundur dan harus melanjutkan mengikuti anak-anak kiri.

Ini kode Java saya (Maaf, ini bukan 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
Saya sangat menyukai jawaban Anda karena memberikan alasan tingkat tinggi untuk menghasilkan solusi ini!
KFL

6

Saya telah membuat animasi untuk algoritme di sini: https://docs.google.com/presentation/d/11GWAeUN0ckP7yjHrQkIB0WT9ZUhDBSa-WR0VsPU38fg/edit?usp=sharing

Ini semoga membantu untuk memahami. Lingkaran biru adalah kursor dan setiap slide merupakan iterasi dari loop sementara luar.

Berikut kode untuk morris traversal (saya menyalin dan memodifikasinya dari geeks untuk 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)

Animasi Anda cukup menarik. Harap pertimbangkan untuk membuatnya menjadi gambar yang akan dimasukkan ke dalam posting Anda, karena tautan eksternal sering kali mati setelah beberapa waktu.
laancelot

1
Animasi sangat membantu!
yyFred

spreadsheet yang bagus dan penggunaan perpustakaan binarytree. tetapi kodenya tidak benar, gagal mencetak node root. Anda perlu menambahkan baris print(cursor.value)setelahpre.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
            }        
        }
    }

Saya pikir kode ini akan lebih baik untuk dipahami, cukup gunakan null untuk menghindari loop tak terbatas, tidak harus menggunakan sihir lagi. Ini dapat dengan mudah dimodifikasi untuk memesan sebelumnya.


1
Solusinya sangat rapi tapi ada satu masalah. Menurut Knuth pohon itu tidak boleh dimodifikasi pada akhirnya. Dengan melakukan temp.left = nullpohon akan hilang.
Ankur

Metode ini dapat digunakan di tempat-tempat seperti mengubah pohon biner menjadi daftar tertaut.
cyber_raj

Seperti yang dikatakan @Shan, algoritme tidak boleh mengubah pohon aslinya. Sementara algoritme Anda berfungsi untuk menjelajahinya, algoritme tersebut menghancurkan pohon aslinya. Oleh karena itu, ini sebenarnya berbeda dari algoritme asli dan karena itu menyesatkan.
ChaoSXDemon


1

Saya harap pseudo-code di bawah ini lebih mengungkapkan:

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

Mengacu pada kode C ++ dalam pertanyaan, loop sementara dalam menemukan pendahulu dalam urutan dari node saat ini. Dalam pohon biner standar, anak kanan pendahulu harus nol, sedangkan dalam versi berulir, anak kanan harus menunjuk ke simpul saat ini. Jika anak yang tepat adalah null, itu diatur ke node saat ini, yang secara efektif menciptakan threading , yang digunakan sebagai titik kembali yang seharusnya disimpan, biasanya di tumpukan. Jika anak kanan bukan nol, maka algoritme memastikan bahwa pohon asli dipulihkan, dan kemudian melanjutkan traversal di subtree kanan (dalam hal ini diketahui bahwa subtree kiri dikunjungi).


0

Solusi Python Kompleksitas Waktu: O (n) Kompleksitas Ruang: O (1)

Penjelasan Penjelajahan Inorder Morris yang Sangat Baik

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

Maaf, tapi sayangnya ini bukan jawaban langsung untuk pertanyaan itu. OP meminta penjelasan tentang cara kerjanya, bukan implementasinya, mungkin karena mereka ingin menerapkan algoritme itu sendiri. Komentar Anda bagus untuk seseorang yang sudah memahami algoritme, tetapi OP belum. Selain itu, sebagai kebijakan, jawaban harus berdiri sendiri, bukan hanya menautkan ke beberapa sumber daya luar, karena tautan dapat berubah atau rusak seiring waktu. Tidak masalah untuk menyertakan tautan, tetapi jika Anda melakukannya, Anda juga harus menyertakan setidaknya inti dari apa yang disediakan tautan tersebut.
Anonim

0

Penjelasan PFB tentang 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;
        }
    }
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.