二叉樹遍歷詳解(遞歸遍歷、非遞歸棧遍歷,Morris遍歷)

一、前言

《二叉查找樹全面詳細介紹》中講解了二叉樹操作:搜索(查找)、遍歷、插入、刪除。其中遍歷深度優先遍歷(DFS)按照實現方法可以分爲:遞歸遍歷實現、非遞歸遍歷實現、Morris遍歷實現,文中只給了代碼,沒有對實現過程進行講解,本文將對遞歸遍歷實現、非遞歸遍歷棧實現、Morris遍歷實現這三類實現方法進行講解。

 

二、三類實現方法特點

遞歸實現

  • 編碼簡單,易於理解;
  • 隱式使用棧存儲當前沒有處理完的節點信息;
  • 若樹深度大,遞歸深度大可能會超過堆棧保留大小;

時間複雜度:O(n),空間複雜度:O(logn)

 

非遞歸棧實現

  • 顯示使用棧存儲當前沒有處理完的節點信息,需要花費額外時間來維護棧併爲棧留出空間;
  • 若樹結構不是理想結構(呈線性)棧可能需要保存樹中幾乎所有節點信息,當樹很大的時候這會是一個嚴重問題;
  • 效率相對遞歸實現會高一些,實現起來會比遞歸實現複雜一些;

時間複雜度:O(n),空間複雜度:O(logn)

 

Morris遍歷

  • 佔用空間少,不用額外花費時間維護棧和爲棧留出空間;
  • 利用了樹中空節點;
  • 實現複雜,理解有一定難度;

時間複雜度:O(n),空間複雜度:O(1)

 

三、遞歸遍歷實現

遞歸深度遍歷是最常見的寫法,也最好理解,直接看代碼。

// 前序遍歷,遞歸實現
template<class T>
void BST<T>::preorder(BSTNode<T>* p)
{
	if (p != 0)
	{
        visit(p);
        preorder(p->m_left);
        preorder(p->m_right);
	}
}

// 中序遍歷,遞歸實現
template<class T>
void BST<T>::inorder(BSTNode<T>* p) 
{
    if (p != 0) 
    {
        inorder(p->m_left);
        visit(p);
        inorder(p->m_right);
    }
}

// 後序遍歷,遞歸實現
template<class T>
void BST<T>::postorder(BSTNode<T>* p) 
{
    if (p != 0) 
    {
        postorder(p->m_left);
        postorder(p->m_right);
        visit(p);
    }
}

 

四、非遞歸遍歷實現

4.1 前序遍歷,非遞歸棧實現

4.1.1 實現步驟

  1. 起始將樹根節點添加到棧中;
  2. 棧彈出一個元素,訪問該節點,並將該節點的右子節點和左子節點添加到棧中。說明,節點爲空不用添加;
  3. 判斷棧是否爲空,爲空樹遍歷結束,不爲空繼續步驟2。

4.1.2 實例演示

棧的變化過程

4.1.3 編碼實現

// 前序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativePreorder() 
{
    Stack<BSTNode<T>*> travStack;
    BSTNode<T>* p = root;
    if (p != 0) 
    {
        travStack.push(p);
        while (!travStack.empty())
        {
            p = travStack.pop();
            visit(p);
            if (p->m_right != 0)
                travStack.push(p->m_right);

            if (p->m_left != 0)            
                travStack.push(p->m_left); 
        }
    }
}

4.2 中序遍歷,非遞歸棧實現

4.2.1 實現步驟

中序遍歷,非遞歸棧實現,提供了兩種方法,思路差不多,做了一些改動,方法一iterativeInorder是《C++數據結構與算法》中提供的實現方法;方法二iterativeInorder_2,做了一些改動,思想是一樣的,個人認爲方法二更好一點點,這裏實現步驟和思路演示用的是方法二。

  1. 將樹根節點賦值給變量p;
  2. 循環判斷p是否爲空,非空執行循環;
  3. 循環中將p節點添加到棧中,訪問p節點的左節點,非空就添加到棧中,繼續訪問p節點左節點的左節點非空就添加到棧中,直到左節點爲空;
  4. 彈出棧頂元素將其賦值給p,並訪問該元素;
  5. 棧不爲空且彈出的棧頂元素沒有右節點,繼續彈出棧元素並訪問它直到,棧爲空或者彈出的節點有右節點;
  6. 將最後一個彈出的節點右節點賦值給p,回到步驟2,繼續執行;

4.2.2 實例演示

棧的變化過程

4.2.3 編碼實現

// 中序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativeInorder() 
{
    Stack<BSTNode<T>*> travStack;
    BSTNode<T>* p = root;
    while (p != nullptr) 
    {
        while (p != nullptr)
        {                 
            if (p->m_right)                
                travStack.push(p->m_right); 

            travStack.push(p);
            p = p->m_left;
        }

        p = travStack.pop();             
        while (!travStack.empty() && p->m_right == nullptr)
        { 
            visit(p);                                
            p = travStack.pop();
        }

        visit(p);                       
        if (!travStack.empty())          
            p = travStack.pop();
        else 
            p = nullptr;
    }
}

// 中序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativeInorder_2() 
{
    Stack<BSTNode<T>*> travStack;
    BSTNode<T>* p = root;
    while (p != nullptr) 
    {
        travStack.push(p);
        for (; p != nullptr && p->m_left != nullptr; p = p->m_left)
            travStack.push(p->m_left); 

        p = travStack.pop(); 
        visit(p);
        while (!travStack.empty() && p->m_right == nullptr)
        { 
            p = travStack.pop();
            visit(p);                                
        }

        p = p->m_right;
    }
}

 

4.3 後序遍歷,非遞歸棧實現

和中序遍歷非遞歸棧實現有些類似,不同的是:

中序遍歷彈出左節點之後,繼續彈出父節點,然後判斷父節點是否有右節點,沒有繼續彈出下一個節點;有則以這個右節點爲節點進行下一次循環遍歷。

後序遍歷彈出左節點之後,繼續彈出父節點,然後判斷父節點是否有右節點,沒有繼續彈出下一個節點;有右節點且這個右節點之前已經訪問過,繼續彈出下一個節點;有右節點且這個右節點之前沒有訪問過,則以這個右節點爲節點進行下一次循環遍歷。

最主要的是需要理解當一個節點的右節點已經被訪問了,則它的右節點不需要再被訪問。

4.3.1 實現步驟

  1. 將樹根節點賦值給變量p,定義一個標記變量guard將根節點賦值給它;
  2. 循環判斷p是否爲空,非空執行循環;
  3. 循環中將p節點添加到棧中,訪問p節點的左節點,非空就添加到棧中,繼續訪問p節點左節點的左節點非空就添加到棧中,直到左節點爲空;
  4. 將p賦值給guard,彈出棧頂元素將其賦值給p,並訪問該元素;
  5. 棧不爲空且彈出的棧頂元素沒有右節點或者有右節點但該右節點和guard相等(也就是右節點剛剛被訪問過),繼續彈出棧元素並訪問它直到,棧爲空或者彈出的節點有右節點且沒有被訪問;
  6. 將最後一個彈出的節點右節點賦值給p,回到步驟2,繼續執行

4.3.2 實例演示

棧的變化過程

說明:在棧變化過程中的第四個棧,棧頂是D,在對D節點右訪問的時候,發現節點I已經被訪問過,此時不需要再訪問D的右節點,直接輸出D節點並彈出D節點即可。

4.3.3 編碼實現

// 後序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativePostorder() 
{
    Stack<BSTNode<T>*> travStack;
    BSTNode<T>* p = root, * guard = root;
    while (p != nullptr) 
    {
        for (; p->m_left != nullptr; p = p->m_left)
            travStack.push(p);

        while (p->m_right == nullptr || p->m_right == guard)
        {
            visit(p);
            if (travStack.empty())
                return;

            guard = p;
            p = travStack.pop();
        }

        travStack.push(p);
        p = p->m_right;
    }
}

 

五、Morris遍歷

利用空閒節點找到回去的路,和避免走重複的路。

5.1 前序遍歷,Morris遍歷算法實現

5.1.1 實現步驟

  1. 將樹根節點賦值給變量p;
  2. 若p節點沒有左節點,輸出p節點,並將p指向它的右節點
  3. 若p節點有左節點,找到p的左節點中最後訪問節點(temp)
  4. 若temp節點右節點爲空,temp的右節點指向p節點,輸出p節點,並將p指向它的左節點
  5. 若temp節點右節點不爲空,temp的右節點設置爲空(恢復原始狀態),輸出p節點,並將p指向它的右節點
  6. 會到步驟2

5.1.2 實例演示

1) 樹結構如下:

2)p指向節點A,A左節點中最後訪問節點是E,將E的指向A,輸出節點A。p指向A左節點B。

3)p指向節點B,B左節點中最後訪問節點是D,將D的指向B,輸出節點B。p指向B左節點D。

4)p指向節點D,D沒左節點,輸出節點D,p指向D右節點B。

5)p指向節點B,B左節點中最後訪問節點是D,由於D的右節點指向B,說明D節點已經被訪問了,不需要重複訪問,恢復指針狀態,將D的右指針設置爲空。p指向B的右節點E。

6)p指向節點E,E沒有左節點,輸出節點E,p指向E的右節點A。

說明:這裏E沒有左右節點,如果有左右節點處理過程和A節點是一樣的。

7)p指向節點A,A左節點中最後訪問節點是E,由於E的右節點指向A,說明A節點已經被訪問了,不需要重複訪問,恢復指針狀態,將E的右指針設置爲空。p指向A的右節點C。過程同5。

8)p指向節點C,C有左節點,輸出節點C,p指向C的右節點,p爲空遍歷結束。

5.1.3 編碼實現

// 前序遍歷,非遞歸Morris遍歷算法實現
template<class T>
void BST<T>::MorrisPreorder() 
{
    BSTNode<T>* p = root, * tmp;
    while (p != nullptr) 
    {
        if (p->m_left == nullptr)
        {
            visit(p);
            p = p->m_right;
        }
        else 
        {
            tmp = p->m_left;
            while (tmp->m_right != nullptr && tmp->m_right != p)
                tmp = tmp->m_right;   

            if (tmp->m_right == nullptr)
            {  
                visit(p);         
                tmp->m_right = p;    
                p = p->m_left;        
            }
            else 
            {                   
                tmp->m_right = nullptr;    
                p = p->m_right;       
            }                        
        }
    }
}

 

5.2 中序遍歷,Morris遍歷算法實現

5.2.1 實現步驟

和前序遍歷,Morris遍歷算法類似,只是輸出節點順序有一些變動。直接看代碼

5.2.2 實例演示

演示的圖和前序是一樣的,只是輸出的節點順序不一樣,如下圖紅色點爲輸出的節點。

5.2.3 編碼實現

// 中序遍歷,非遞歸Morris遍歷算法實現
template<class T>
void BST<T>::MorrisInorder() 
{
    BSTNode<T>* p = root, * tmp;
    while (p != 0)
    {
        if (p->m_left == nullptr) 
        {
            visit(p);
            p = p->m_right;
        }
        else 
        {
            tmp = p->m_left;
            while (tmp->m_right != 0 && tmp->m_right != p)  
                tmp = tmp->m_right;   

            if (tmp->m_right == nullptr)
            {   
                tmp->m_right = p;    
                p = p->m_left;       
            }
            else 
            {                   
                visit(p);      
                tmp->m_right = nullptr;    
                p = p->m_right;      
            }                      
        }
    }
}

5.3 中序遍歷,Morris遍歷算法實現

5.3.1 實現步驟

直接看演示圖和代碼吧,有點複雜,需要細看,圖細解!

5.3.2 實例演示

約定:對於有左節點的父節點,我們需要找到父節點的左節點,並沿着這個左節點一直找它的右節點直到葉子節點爲止,然後將右節點指向起始的父節點,這個最右節點查找等價於,查找一顆二叉搜索左節點中最大的一個節點。有點繞,後面需要用到,直接看下面圖解就很容易理解。

1)待遍歷的樹

2)新加一個節點,它的左節點指向樹的根節點。新加節點是爲了能在遍歷的規則下輸出樹根節點。

3)p指向節點R,p的左節點中最大的節點是A,將A的右節點指向R,p指向R節點的左節點A。

4)p指向節點A,A的左節點中最大的節點是H,將H的右節點指向A,p指向A節點的左節點B。

5)p指向節點B,B的左節點中最大的節點是C,將C的右節點指向B,p指向B節點的左節點C。

6) p指向節點C,C沒有左節點,p指向C節點的右節點B。

7) p指向節點B,B節點的左節點是C,C右節點指向了B自己,輸出節點C,p指向B節點的右節點D

8)p指向節點D,D沒有左節點,P指向D的右節點E。

9)p指向節點E,E的左節點中最大的節點是F,將F的右節點指向E,p指向E節點的左節點F。

10)p指向節點F,F沒有左節點,p指向F節點的右節點E。

11) p指向節點E,E節點的左節點是F,F右節點指向了E自己,輸出節點F,p指向E節點的右節點G

12)p指向節點G,G沒有左節點,P指向G的右節點H。

13)p指向節點H,H的左節點中最大的節點是I,將I的右節點指向H,p指向H節點的左節點I。

14)p指向節點I,I沒有左節點,p指向I節點的右節點H。

15)p指向節點H,H節點的左節點是I,I右節點指向了H自己,輸出節點I,p指向H節點的右節點A。

16)p指向節點A,p左節點最大節點是A,指向了自己,將A節點的右節點指向順序進行翻轉。在翻轉過程中,q指向最後一個右節點,R指向了起始節點A,對S指針賦值爲H的右節點G,通過這些條件,我們可以從下往上遍歷起始的節點左節點的右節點。

輸出節點的順序是H、G、E、D、B,並恢復指針指向狀態。p指向A節點的右節點R。

17)p指向節點R,R節點的左節點是A,A右節點指向了R自己,輸出節點A。遍歷完成。

總結一下:

最主要的是需要理解步驟16,先進行一次翻轉,然後輸出節點恢復指針狀態。

5.3.3 編碼實現

// 後序遍歷,非遞歸Morris遍歷算法實現
template<class T>
void BST<T>::MorrisPostorder() 
{
    BSTNode<T>* p = new BSTNode<T>(), * tmp, * q, * r, * s;
    p->m_left = root;
    while (p != 0)
    {
        if (p->m_left == nullptr)
            p = p->m_right;
        else
        {
            tmp = p->m_left;
            while (tmp->m_right != nullptr && tmp->m_right != p)  
                tmp = tmp->m_right;   

            if (tmp->m_right == nullptr) 
            {   
                tmp->m_right = p;     
                p = p->m_left;        
            }
            else
            {             
                for (q = p->m_left, r = q->m_right, s = r->m_right;
                    r != p; q = r, r = s, s = s->m_right)
                    r->m_right = q;

                for (s = q->m_right; q != p->m_left;
                    q->m_right = r, r = q, q = s, s = s->m_right)
                    visit(q);

                visit(p->m_left);    
                tmp->m_right = nullptr;     
                p = p->m_right;      
            }                   
        }
    }
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章