1. 程式人生 > >常見的二叉樹面試題

常見的二叉樹面試題

    面試中,最常見是資料結構就是二叉樹和連結串列了,其中和二叉樹有關的常見面試題主要是:樹的前序遍歷、中序遍歷、後序遍歷、分層遍歷、樹的節點數、樹的葉子節點數、樹的第K層節點數、樹的深度、樹的寬度、平衡二叉樹的判定、完全二叉樹的判定、滿二叉樹的判定、由前序中序反推後序遍歷、由前序中序重建二叉樹,處理這些問題基本思想無外乎是“遍歷+遞迴”。

    關於樹的遍歷,遞迴方式太傻太天真,但也是最基本的思想,實現程式碼網上資料可謂是汗牛充棟,在此就不贅述了。

    下面僅列舉了非遞迴實現樹的前序、中序以及後序遍歷,由於筆者對程式碼有潔癖,最不能忍受一個while迴圈裡再巢狀另外一個while迴圈(網上千篇一律的實現程式碼方式),雖然二者的思想都是一致的,但筆者的實現方式更為直觀易懂,故此處的版本都只借助一個棧、一個while迴圈:

前序非遞迴遍歷:

void PreOrderTraseNonRecursion(BTree* t)
{
	stack<BNode*> s;
	s.push(t);
	while(!s.empty())
	{
		BNode* p = s.top();
		s.pop();
		Visit(p);
		if(p->rchild)
			s.push(p->rchild);
		if(p->lchild)
			s.push(p->lchild);
	}
}

中序非遞迴遍歷:

void InOrderTraseNonRecursion(BTree* t)
{
	stack<BNode*> s;
	BNode* p=t;
	while(p||!s.empty())
	{
		if(p)
		{
			s.push(p);
			p=p->lchild;
		}
		else
		{
			p = s.top();
			s.pop();
			Visit(p);
			p=p->rchild;
		}
	}
}
前序和中序的非遞迴遍歷都很好理解,我就不再浪費口舌筆墨了,要是真看不懂,請回去把嚴蔚敏那本《資料結構》再啃一遍、兩遍、三遍……要是還看不懂,我只能懷著無比沉重的心情告訴你一個非常不幸的訊息:“對不起,你不適合這個行業!”

後序非遞迴遍歷:後序非遞迴遍歷稍微比較難理解些,但是你如果對前序和中序遍歷思想已經融會貫通了,其實也就那樣:

從根節點開始,只要當前節點存在,或者棧不為空,則重複下面操作: 

(1)從當前節點開始,進棧並走左子樹,直到左子樹為空。 

(2)如果棧頂節點的右子樹為空,或者棧頂節點的右孩子為剛訪問過的節點,

   則退棧並訪問,然後置當前節點指標為空。

(3)否則走右子樹。

void PostOrderTraseNonRecursion(BTree* t)
{
	stack<BNode*> s;
	BNode* p=t;
	BNode* q=NULL;
	while(p||!s.empty())
	{
		if(p)
		{
			s.push(p);
			p=p->lchild;
		}
		else
		{
			p = s.top();
			if(p->rchild==NULL || p->rchild==q)
			{
				Visit(p);
				s.pop();
				q = p;
				p = NULL;
			}
			else
			{
				p=p->rchild;
			}
		}
	}
}
還有一種網上流傳的後序遍歷的方式是藉助於兩個棧,一個棧用來遍歷,另一個棧儲存遍歷到的結點,最後一起出棧,就是後序遍歷結果。
void PostOrderTraseNonRecursion2(BTree* t)  
{  
	stack<BNode*> sTraverse,sVisit;  
	BNode* p=NULL;  
	if(t==NULL) 
		return;  
	sTraverse.push(t);  
	while(!sTraverse.empty())  
	{  
		p=sTraverse.top(); 
		sTraverse.pop();  
		sVisit.push(p);  
		if(p->lchild) 
			sTraverse.push(p->lchild);  
		if(p->rchild) 
			sTraverse.push(p->rchild);  
	}  

	while(!sVisit.empty())  
	{  
		p=sVisit.top(); 
		sVisit.pop();  
		Visit(p);
	}  
}
分層遍歷:通過分層遍歷可實現統計樹的深度、每一層節點數、樹的最大寬度等要求。基本解法想必你也知道了,就是藉助一個佇列,進行入隊、出隊操作,如下面的二叉樹,要是一口氣全部列印所有節點,即輸出: 1 2 3 4 5 6 7 再進一步考察,如何實現將二叉樹按層列印,即每一行列印二叉樹的每一層樹節點,輸出為: 1 2 3  4 5 6 7            1
          / \
         2   3
        / \   \
       4   5   6
      /
     7
這就需要再基本的分層遍歷基礎上再做些手腳了,下面是一個簡單的例子:
//分層列印節點,每一行為一層,返回該二叉樹的層數
int DepthTrase(BTree* t)
{
	if(t==NULL)
		return 0;

	vector<BNode*> v;
	v.push_back(t);

	int front=0;//該層的開始節點下標
	int rear=0;//該層結束節點下標
	int last=1;//所有已遍歷的節點個數
	int level=0;//層次數

	while(rear < v.size())
	{
		BNode* p = v[rear++];
		if(p->lchild)
			v.push_back(p->lchild);
		if(p->rchild)
			v.push_back(p->rchild);

		if(rear>=last)
		{
			//列印每一層節點
			for(int i=front;i<rear;++i)
			{
				Visit(v[i]);
			}
			cout<<endl;

		        //重置 為下一層遍歷準備
			front=rear;
			last=v.size();
			
			level++;
		}
	}
	return level;
}

下面再介紹幾個二叉樹中的高階應用: 1、根節點到r節點的路徑:    後序遍歷時訪問到r節點時,棧中的所有的節點均為r節點的祖先,這些祖先構成根節點到r節點的路徑。    由此引出下面的第2個問題…… 2、求二叉樹中兩個節點的最低公共祖先節點:
     遞迴解法效率很低,有很多重複的遍歷,不再贅述。
      非遞迴解法:先求從根節點到兩個節點的路徑,然後再比較對應路徑的節點,最後一個相同的節點也就是他們在二叉樹中的最低公共祖先節點,問題轉換為求兩個單鏈表的最後一個公共節點。
3、二叉樹節點最大距離

姑且定義"距離"為兩節點之間邊的個數。求二叉樹中相距最遠的兩個節點之間的距離,如下圖: