Morris Traversal方法遍歷二叉樹(shù)(非遞歸,不用棧,O(1)空間)

本文主要解決一個(gè)問(wèn)題,如何實(shí)現(xiàn)二叉樹(shù)的前中后序遍歷,有兩個(gè)要求:

  1. O(1)空間復(fù)雜度,即只能使用常數(shù)空間;
  2. 二叉樹(shù)的形狀不能被破壞(中間過(guò)程允許改變其形狀)。
    通常,實(shí)現(xiàn)二叉樹(shù)的前序(preorder)、中序(inorder)、后序(postorder)遍歷有兩個(gè)常用的方法:一是遞歸(recursive),二是使用棧實(shí)現(xiàn)的迭代版本(stack+iterative)。這兩種方法都是O(n)的空間復(fù)雜度(遞歸本身占用stack空間或者用戶自定義的stack),所以不滿足要求。

Morris Traversal方法可以做到這兩點(diǎn),與前兩種方法的不同在于該方法只需要O(1)空間,而且同樣可以在O(n)時(shí)間內(nèi)完成。

要使用O(1)空間進(jìn)行遍歷,最大的難點(diǎn)在于,遍歷到子節(jié)點(diǎn)的時(shí)候怎樣重新返回到父節(jié)點(diǎn)(假設(shè)節(jié)點(diǎn)中沒(méi)有指向父節(jié)點(diǎn)的p指針),由于不能用棧作為輔助空間。為了解決這個(gè)問(wèn)題,Morris方法用到了線索二叉樹(shù)(threaded binary tree)的概念。在Morris方法中不需要為每個(gè)節(jié)點(diǎn)額外分配指針指向其前驅(qū)(predecessor)和后繼節(jié)點(diǎn)(successor),只需要利用葉子節(jié)點(diǎn)中的左右空指針指向某種順序遍歷下的前驅(qū)節(jié)點(diǎn)或后繼節(jié)點(diǎn)就可以了。

Morris只提供了中序遍歷的方法,在中序遍歷的基礎(chǔ)上稍加修改可以實(shí)現(xiàn)前序,而后續(xù)就要再費(fèi)點(diǎn)心思了。所以先從中序開(kāi)始介紹。

首先定義在這篇文章中使用的二叉樹(shù)節(jié)點(diǎn)結(jié)構(gòu),即由val,left和right組成:

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

一、中序遍歷

步驟:

  1. 如果當(dāng)前節(jié)點(diǎn)的左孩子為空,則輸出當(dāng)前節(jié)點(diǎn)并將其右孩子作為當(dāng)前節(jié)點(diǎn)。

  2. 如果當(dāng)前節(jié)點(diǎn)的左孩子不為空,在當(dāng)前節(jié)點(diǎn)的左子樹(shù)中找到當(dāng)前節(jié)點(diǎn)在中序遍歷下的前驅(qū)節(jié)點(diǎn)。

    a) 如果前驅(qū)節(jié)點(diǎn)的右孩子為空,將它的右孩子設(shè)置為當(dāng)前節(jié)點(diǎn)。當(dāng)前節(jié)點(diǎn)更新為當(dāng)前節(jié)點(diǎn)的左孩子。

    b) 如果前驅(qū)節(jié)點(diǎn)的右孩子為當(dāng)前節(jié)點(diǎn),將它的右孩子重新設(shè)為空(恢復(fù)樹(shù)的形狀)。輸出當(dāng)前節(jié)點(diǎn)。當(dāng)前節(jié)點(diǎn)更新為當(dāng)前節(jié)點(diǎn)的右孩子。

  3. 重復(fù)以上1、2直到當(dāng)前節(jié)點(diǎn)為空。

圖示:

下圖為每一步迭代的結(jié)果(從左至右,從上到下),cur代表當(dāng)前節(jié)點(diǎn),深色節(jié)點(diǎn)表示該節(jié)點(diǎn)已輸出。

Morris Inorder Traversal

代碼:

void inorderMorrisTraversal(TreeNode *root) {
    TreeNode *cur = root, *prev = NULL;
    while (cur != NULL)
    {
        if (cur->left == NULL)          // 1.
        {
            printf("%d ", cur->val);
            cur = cur->right;
        }
        else
        {
            // find predecessor
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)   // 2.a)
            {
                prev->right = cur;
                cur = cur->left;
            }
            else                       // 2.b)
            {
                prev->right = NULL;
                printf("%d ", cur->val);
                cur = cur->right;
            }
        }
    }
}

復(fù)雜度分析:

空間復(fù)雜度:O(1),因?yàn)橹挥昧藘蓚€(gè)輔助指針。

時(shí)間復(fù)雜度:O(n)。證明時(shí)間復(fù)雜度為O(n),最大的疑惑在于尋找中序遍歷下二叉樹(shù)中所有節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)的時(shí)間復(fù)雜度是多少,即以下兩行代碼:

while (prev->right != NULL && prev->right != cur)
     prev = prev->right;

直覺(jué)上,認(rèn)為它的復(fù)雜度是O(nlgn),因?yàn)檎覇蝹€(gè)節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)與樹(shù)的高度有關(guān)。但事實(shí)上,尋找所有節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)只需要O(n)時(shí)間。n個(gè)節(jié)點(diǎn)的二叉樹(shù)中一共有n-1條邊,整個(gè)過(guò)程中每條邊最多只走2次,一次是為了定位到某個(gè)節(jié)點(diǎn),另一次是為了尋找上面某個(gè)節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn),如下圖所示,其中紅色是為了定位到某個(gè)節(jié)點(diǎn),黑色線是為了找到前驅(qū)節(jié)點(diǎn)。所以復(fù)雜度為O(n)。

Morris Inorder Traversal

二、前序遍歷

前序遍歷與中序遍歷相似,代碼上只有一行不同,不同就在于輸出的順序。

步驟:

  1. 如果當(dāng)前節(jié)點(diǎn)的左孩子為空,則輸出當(dāng)前節(jié)點(diǎn)并將其右孩子作為當(dāng)前節(jié)點(diǎn)。

  2. 如果當(dāng)前節(jié)點(diǎn)的左孩子不為空,在當(dāng)前節(jié)點(diǎn)的左子樹(shù)中找到當(dāng)前節(jié)點(diǎn)在中序遍歷下的前驅(qū)節(jié)點(diǎn)。

    a) 如果前驅(qū)節(jié)點(diǎn)的右孩子為空,將它的右孩子設(shè)置為當(dāng)前節(jié)點(diǎn)。輸出當(dāng)前節(jié)點(diǎn)(在這里輸出,這是與中序遍歷唯一一點(diǎn)不同)。當(dāng)前節(jié)點(diǎn)更新為當(dāng)前節(jié)點(diǎn)的左孩子。

    b) 如果前驅(qū)節(jié)點(diǎn)的右孩子為當(dāng)前節(jié)點(diǎn),將它的右孩子重新設(shè)為空。當(dāng)前節(jié)點(diǎn)更新為當(dāng)前節(jié)點(diǎn)的右孩子。

  3. 重復(fù)以上1、2直到當(dāng)前節(jié)點(diǎn)為空。

圖示:

Morris Preorder Traversal

代碼

void preorderMorrisTraversal(TreeNode *root) {
    TreeNode *cur = root, *prev = NULL;
    while (cur != NULL)
    {
        if (cur->left == NULL)
        {
            printf("%d ", cur->val);
            cur = cur->right;
        }
        else
        {
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)
            {
                printf("%d ", cur->val);  // the only difference with inorder-traversal
                prev->right = cur;
                cur = cur->left;
            }
            else
            {
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}

復(fù)雜度分析:

時(shí)間復(fù)雜度與空間復(fù)雜度都與中序遍歷時(shí)的情況相同。

三、后序遍歷

后續(xù)遍歷稍顯復(fù)雜,需要建立一個(gè)臨時(shí)節(jié)點(diǎn)dump,令其左孩子是root。并且還需要一個(gè)子過(guò)程,就是倒序輸出某兩個(gè)節(jié)點(diǎn)之間路徑上的各個(gè)節(jié)點(diǎn)。

步驟:

當(dāng)前節(jié)點(diǎn)設(shè)置為臨時(shí)節(jié)點(diǎn)dump。

  1. 如果當(dāng)前節(jié)點(diǎn)的左孩子為空,則將其右孩子作為當(dāng)前節(jié)點(diǎn)。

  2. 如果當(dāng)前節(jié)點(diǎn)的左孩子不為空,在當(dāng)前節(jié)點(diǎn)的左子樹(shù)中找到當(dāng)前節(jié)點(diǎn)在中序遍歷下的前驅(qū)節(jié)點(diǎn)。

    a) 如果前驅(qū)節(jié)點(diǎn)的右孩子為空,將它的右孩子設(shè)置為當(dāng)前節(jié)點(diǎn)。當(dāng)前節(jié)點(diǎn)更新為當(dāng)前節(jié)點(diǎn)的左孩子。

    b) 如果前驅(qū)節(jié)點(diǎn)的右孩子為當(dāng)前節(jié)點(diǎn),將它的右孩子重新設(shè)為空。倒序輸出從當(dāng)前節(jié)點(diǎn)的左孩子到該前驅(qū)節(jié)點(diǎn)這條路徑上的所有節(jié)點(diǎn)。當(dāng)前節(jié)點(diǎn)更新為當(dāng)前節(jié)點(diǎn)的右孩子。

  3. 重復(fù)以上1、2直到當(dāng)前節(jié)點(diǎn)為空。

圖示:

Morris Postorder Traversal

代碼:

void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
{
    if (from == to)
        return;
    TreeNode *x = from, *y = from->right, *z;
    while (true)
    {
        z = y->right;
        y->right = x;
        x = y;
        y = z;
        if (x == to)
            break;
    }
}

void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
{
    reverse(from, to);
    
    TreeNode *p = to;
    while (true)
    {
        printf("%d ", p->val);
        if (p == from)
            break;
        p = p->right;
    }
    
    reverse(to, from);
}

void postorderMorrisTraversal(TreeNode *root) {
    TreeNode dump(0);
    dump.left = root;
    TreeNode *cur = &dump, *prev = NULL;
    while (cur)
    {
        if (cur->left == NULL)
        {
            cur = cur->right;
        }
        else
        {
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)
            {
                prev->right = cur;
                cur = cur->left;
            }
            else
            {
                printReverse(cur->left, prev);  // call print
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}

復(fù)雜度分析:

空間復(fù)雜度同樣是O(1);時(shí)間復(fù)雜度也是O(n),倒序輸出過(guò)程只不過(guò)是加大了常數(shù)系數(shù)。

參考:

http://www.geeksforgeeks.org/inorder-tree-traversal-without-recursion-and-without-stack/
http://www.geeksforgeeks.org/morris-traversal-for-preorder/
http://stackoverflow.com/questions/6478063/how-is-the-complexity-of-morris-traversal-on
http://blog.csdn.net/wdq347/article/details/8853371
Data Structures and Algorithms in C++ by Adam Drozdek

本文轉(zhuǎn)自Morris Traversal方法遍歷二叉樹(shù)(非遞歸,不用棧,O(1)空間)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容