二叉樹的遍歷(深度優先+廣度優先)


二叉樹的遍歷分爲兩類,一類是深度優先遍歷,一類是廣度優先遍歷。

1.深度優先遍歷

二叉樹的深度優先遍歷有三種方式,先序(先根次序)、中序(中根次序)和後序(後根次序)遍歷。

因爲樹的定義本身是遞歸定義,因此採用遞歸的方法去實現樹的三種遍歷不僅容易理解而且代碼很簡潔。若採用非遞歸的方法遍歷二叉樹,需要採用棧去模擬實現。在三種遍歷中,前序和中序遍歷的非遞歸算法都很容易實現,非遞歸後序遍歷實現起來相對來說要難一點。下面一一講解具體的遞歸和非遞歸實現。

1.1 先序遍歷

先根次序遍歷按照“根結點 > 左孩子 > 右孩子”的順序進行訪問。

  • 遞歸實現
//先根遞歸遍歷
void preOrderRecursion(BinaryTreeNode* root)
{
	if(root==NULL) return;
	cout<< " " << root->m_key;
	preOrderRecursion(root->m_pLeft);
	preOrderRecursion(root->m_pRight);
}
  • 非遞歸實現

根據先序遍歷的順序,首先訪問根結點,然後再分別訪問左孩子和右孩子。即對於任一結點,其可看做是根結點,因此可以直接訪問,訪問完之後,若其左孩子不爲空,按相同規則訪問它的左子樹;當訪問完左子樹時,再訪問它的右子樹。因此其處理過程如下:

給定二叉樹的根結點 R:
(1)並將根結點 R 入棧;
(2)判斷棧是否爲空,若不爲空,取棧頂元素 cur 訪問並出棧。然後先將 cur 的右子結點入棧,再將 cur 的左子結點入棧;
(3)重複(3)直到棧空,則遍歷結束。

// 先序非遞歸遍歷,需要使用棧
void preOrderStack(BinaryTreeNode* root)
{
	if(root==NULL) return;
	
	stack<BinaryTreeNode*> stack;
	stack.push(root);
	BinaryTreeNode* cur=NULL;
	while(!stack.empty())
	{
		cur=stack.top();
		cout<<" "<<cur->m_key; //visit
		stack.pop();
		if(cur->m_pRight!=NULL)
		{
			stack.push(cur->m_pRight);
		}
		if(cur->m_pLeft!=NULL)
		{
			stack.push(cur->m_pLeft);
		}
	}
}

1.2 中序遍歷

中序遍歷按照“左孩子 > 根結點 > 右孩子”的順序進行訪問。

  • 遞歸實現
// 中序遞歸遍歷
void midOrderRecursion(BinaryTreeNode* root){
	if(root==NULL) return;
	midOrderRecursion(root->m_pLeft);
	cout<<" "<<root->m_key;   //visit
	midOrderRecursion(root->m_pRight);
}
  • 非遞歸實現

根據中序遍歷的順序,對於任一結點,先訪問其左孩子,而左孩子又可以看做一根結點,然後繼續訪問其左孩子,直到遇到左孩子爲空才進行訪問,然後按相同的規則訪問其右子樹。因此其處理過程如下:

對於給定的二叉樹根結點 R,
(1)若其左孩子不爲空,循環將 R 及 R 左子樹中的所有結點的左孩子入棧;
(2)取棧頂元素 cur,訪問 cur 並將 cur 出棧。然後對 cur 的右子結點進行步驟(1)那樣的處理;
(3)重複(1)和(2)的操作,直到 cur 爲空且棧爲空。

// 中根非遞歸遍歷,需要使用棧
void midOrderStack(BinaryTreeNode* root)
{
	if(root==NULL) return; 
	
	stack<BinaryTreeNode*> stack;
	BinaryTreeNode* cur=root;
	while(!stack.empty() || cur!=NULL)
	{
        while(cur)
        {  
            stack.push(cur);  
            cur=cur->m_pLeft;  
        }  
        cur=stack.top();  
		cout<<" "<<cur->m_key;   //visit
        stack.pop();  
        cur=cur->m_pRight;  
    }
}

1.3 後序遍歷

後序遍歷按照“左孩子 > 右孩子 > 根結點”的順序進行訪問。

  • 遞歸實現
// 後根遞歸遍歷
void postOrderRecursion(BinaryTreeNode* root)
{
	if(root==NULL) return;
	postOrderRecursion(root->m_pLeft);
	postOrderRecursion(root->m_pRight);
	cout << " " << root->m_key;
}
  • 非遞歸實現

後序遍歷的非遞歸實現是三種遍歷方式中最難的一種。因爲在後序遍歷中,要保證左孩子和右孩子都已被訪問並且左孩子要在右孩子前被訪問,才能訪問根節點。這就爲流程的控制帶來了難題。下面介紹兩種思路。

第一種思路:對於任一結點 P,將其入棧,然後沿其左子樹一直往下搜索,直到搜索到沒有左孩子的結點,此時該結點出現在棧頂,但是此時不能將其出棧並訪問,因此其右孩子還爲被訪問。所以接下來按照相同的規則對其右子樹進行相同的處理,當訪問完其右孩子時,該結點又出現在棧頂,此時可以將其出棧並訪問。這樣就保證了正確的訪問順序。可以看出,在這個過程中,每個結點都兩次出現在棧頂,只有在第二次出現在棧頂時,才能訪問它。因此需要多設置一個變量標識該結點是否是第一次出現在棧頂。

// 非遞歸後序遍歷,版本1
void postOrderStack1(BinaryTreeNode* root)
{
	if(root==NULL) return; 
	
	stack<pair<BinaryTreeNode*, bool>> s;
    pair<BinaryTreeNode*,bool> cur=make_pair(root,true);
    while(cur.first!=NULL||!s.empty())
    {
     	//沿左子樹一直往下搜索,直至出現沒有左子樹的結點
        while(cur.first!=NULL)
        {
            s.push(cur);
			cur=make_pair(cur.first->m_pLeft,true);
        }
        if(!s.empty())
        {
        	//表示是第一次出現在棧頂
            if(s.top().second==true)
            { 
                s.top().second=false;
                cur=make_pair(s.top().first->m_pRight,true); //將當前節點的右節點入棧
            }
            else
            {
            	// 第二次出現在棧頂 
                cout << s.top().first->m_key << " ";
                s.pop();
            }
        }
    }
}

第二種思路:要保證根結點在左孩子和右孩子訪問之後才能訪問,因此對於任一結點R,
(a)先將R入棧。如果P不存在左孩子和右孩子,則可以直接訪問它並出棧;
(b)如果R存在左孩子或者右孩子,但是其左孩子和右孩子都已被訪問過了,則同樣可以直接訪問該結點並出棧;
(c)若非上述兩種情況,則將R的右孩子和左孩子依次入棧,這樣就保證了每次取棧頂元素的時候,左孩子在右孩子前面被訪問,左孩子和右孩子都在父結點前面被訪問

// 非遞歸後序遍歷,版本 2
void postOrderStack2(BinaryTreeNode* root)    
{	
	if(root==NULL) return;
	
    stack<BinaryTreeNode*> s;
    BinaryTreeNode* cur;		//當前結點 
    BinaryTreeNode* pre=NULL;	//前一次訪問的結點
    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        //在判斷當前結點時,左孩子和右孩子都在根結點前已經被訪問
        if((cur->m_pLeft==NULL&&cur->m_pRight==NULL) || (pre!=NULL&&(pre==cur->m_pLeft || pre==cur->m_pRight)))
        {
            cout<<cur->m_key<<" ";  //如果當前結點沒有孩子結點或者孩子節點都已被訪問過 
            s.pop();
            pre=cur; 
        }
        else
        {
            if(cur->m_pRight!=NULL) s.push(cur->m_pRight);
            if(cur->m_pLeft!=NULL) s.push(cur->m_pLeft);
        }
    }    
}

2. 廣度優先遍歷

廣度優先周遊的方式是按層次從上到下,從左到右的逐層訪問,不難想到,可以利用一個隊列來實現。基本思想如下:
(1)首先把二叉樹的根節點送入隊列;
(2)隊首的節點出隊列並訪問之,然後把它的右子節點和左子節點分別入隊列;
(3)重複上面兩步操作,直至隊空。

// 廣度優先遍歷二叉樹,使用隊列實現
void breadthFirstOrder(BinaryTreeNode* root)
{
	if(root==NULL) return;
	queue<BinaryTreeNode*> queue;
	queue.push(root);
	while(!queue.empty())
	{
		BinaryTreeNode* cur=queue.front();
		cout<<" "<<cur->m_key;//visit
		queue.pop();
		if(cur->m_pLeft!=NULL) queue.push(cur->m_pLeft);
		if(cur->m_pRight!=NULL) queue.push(cur->m_pRight);
	}
}    

3.驗證結果

以上面介紹的各種遍歷,驗證代碼如下:

#include  <iostream>
#include <stack>
#include <queue>  
using namespace std;

// 二叉樹節點結構體
struct BinaryTreeNode
{
	int m_key;
	BinaryTreeNode* m_pLeft;
	BinaryTreeNode* m_pRight;
};

/****************************************
func:根據前序序列和中序序列構建二叉樹
para:preOrder:前序序列;midOrder:中序序列;len:節點數
****************************************/
BinaryTreeNode* construct(int* preOrder,int* midOrder,int len)
{
	if(preOrder==NULL||midOrder==NULL||len<=0) return NULL;

	//先序遍歷的第一個值是根結點的鍵值
	int rootKey=preOrder[0];
	BinaryTreeNode* root=new BinaryTreeNode;
	root->m_key=rootKey;
	root->m_pLeft=root->m_pRight=NULL;
	
	//只有一個節點
	if(len==1 && *preOrder == *midOrder) return root;
	
	//在中序遍歷中找到根節點的值
	int* rootMidOrder=midOrder;
	int leftLen=0; //左子樹節點數
	while(*rootMidOrder!=rootKey&&rootMidOrder<=(midOrder+len-1))
	{
		++rootMidOrder;
		++leftLen;
	}
	//在中序序列未找到根結點,輸入錯誤
	if(*rootMidOrder!=rootKey) return NULL;
	
	//構建左子樹
	if(leftLen>0)
	{
		root->m_pLeft=construct(preOrder+1,midOrder,leftLen);
	}
	//構建右子樹
	if(len-leftLen-1>0)
	{
		root->m_pRight=construct(preOrder+leftLen+1,rootMidOrder+1,len-leftLen-1);
	}
	return root;
}

int main()
{
	// 先序序列
	int preOrder[8]={1,2,4,7,3,5,6,8};
	// 中序序列
	int midOrder[8]={4,7,2,1,5,3,8,6};
    // 建樹
	BinaryTreeNode* root=construct(preOrder, midOrder, 8);
	
	cout<<"---preOrder---"<<endl;
	cout<<"recursion version: ";
	preOrderRecursion(root);
	cout<<endl<<"stack version: ";
	preOrderStack(root);
	
	cout<<endl<<endl<<"---midOrder---"<<endl;
	cout<<"recursion version: ";
	midOrderRecursion(root);
	cout<<endl<<"stack version1: ";
	postOrderStack1(root);
	cout<<endl<<"stack version2: ";
	postOrderStack2(root);
	
	cout<<endl<<endl<<"---postOrder---"<<endl;
	cout<<"recursion version: ";
	postOrderRecursion(root);
	cout<<endl<<"stack version: ";
	postOrderStack1(root);
	
	cout<<endl<<endl<<"---Breadth First Order---"<<endl;
	breadthFirstOrder(root);
}

實驗結果如下:

---preOrder---
recursion version:  1 2 4 7 3 5 6 8
stack version:  1 2 4 7 3 5 6 8

---midOrder---
recursion version:  4 7 2 1 5 3 8 6
stack version:  4 7 2 1 5 3 8 6

---postOrder---
recursion version:  7 4 2 5 8 6 3 1
stack version1: 7 4 2 5 8 6 3 1 
stack version2: 7 4 2 5 8 6 3 1 

---Breadth First Order---
 1 2 3 4 5 6 7 8

參考文獻

[1] CSDN.二叉樹的非遞歸遍歷
[2] CSDN.二叉樹簡介與構建

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