中序遍歷和先序遍歷/後序遍歷構建二叉樹
1:問題
給定二叉樹的2個遍歷序列(如先序+中序,先序+後序,中序+後序等),是否能夠根據這2個遍歷序列唯一確定二叉樹?
struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
};
2:理論分析
資料結構的基礎知識中重要的一點就是能否根據兩種不同遍歷序列的組合(有三種:先序+中序,先序+後序,中序+後序),唯一的確定一棵二叉樹。然後就是根據二叉樹的不同遍歷序列(先序、中序、後序),重構二叉樹。顯然,這三種組合並不是都能唯一確定二叉樹的,其中先序+後序就不能唯一確定一棵二叉樹,下面是關於該問題的證明與結論。
證明:因為先序序列的第一個元素是根結點,該元素將二叉樹中序序列分成兩部分,左邊(假設有L個元素)表示左子樹,若左邊無元素,則說明左子樹為空;右邊(假設有R個元素)是右子樹,若為空,則右子樹為空。根據前序遍歷中"根-左子樹-右子樹"的順序,則由從先序序列的第二元素開始的L個結點序列和中序序列根左邊的L個結點序列構造左子樹,由先序序列最後R個元素序列與中序序列根右邊的R個元素序列構造右子樹。
②由中序序列和先序序列能唯一確定一棵二叉樹,但是由先序序列和後序序列不能唯一確定一棵二叉樹,因無法確定左右子樹兩部分。
反例:任何結點只有左子樹的二叉樹和任何結點只有右子樹的二叉樹,其前序序列相同,後序序列相同,但卻是兩棵不同的二叉樹。
③已經說明由二叉樹的先序序列和中序序列可以確定一棵二叉樹,現在來證明由二叉樹的中序序列和後序序列,也可以唯一確定一棵二叉樹。
證明:
當n=1時,只有一個根結點,由中序序列和後序序列可以確定這棵二叉樹。
設當n=m-1時結論成立,即結點數目為m-1時,中序序列和後序序列可以唯一確定二叉樹。現證明當n=m時結論成立。
設中序序列為S1,S2,…,Sm,後序序列是P1,P2,…,Pm。因後序序列最後一個元素Pm是根,則在中序序列中可找到與Pm相等的結點(設二叉樹中各結點互不相同)Si(
若i=1,則S1是根,這時二叉樹的左子樹為空,右子樹的結點數是m-1,則{S2,S3,…,Sm}和{P1,P2,…,Pm-1}可以唯一確定右子樹,從而也確定了二叉樹。
若i=m,則Sm是根,這時二叉樹的右子樹為空,左子樹的結點數是m-1,則{S1,S2,…,Sm-1}和{P1,P2,…,Pm-1}唯一確定左子樹,從而也確定了二叉樹。
最後,當1<i<m時,Si把中序序列分成{S1,S2,…,Si-1}和{Si+1,Si+2,…,Sm}。由於後序遍歷是"左子樹-右子樹-根結點",所以{P1,P2,…,Pi-1}和{Pi,Pi+1,…Pm-1}是二叉樹的左子樹和右子樹的後序遍歷序列。因而由{S1,S2,…,Si-1}和{P1,P2,…,Pi-1}可唯一確定二叉樹的左子樹,由{Si+1,Si+2,…,Sm}和{Pi,Pi+1,…,Pm-1}可唯一確定二叉樹的右子樹。
3、構造思路
1)根據先序遍歷序列和中序遍歷序列構建二叉樹
假定已知二叉樹如下:
___7___ / \ 10 2 / \ / 4 3 8 \ / 1 11
那麼它的先序遍歷和中序遍歷的結果如下:
preorder = {7,10,4,3,1,2,8,11} inorder = {4,10,3,1,7,11,8,2}
需要關注的幾個要點:
1)先序遍歷的第一個結點總是根結點。如上圖中的二叉樹,根結點為7,也是先序遍歷的第一個值。先序遍歷時父親結點總是在孩子結點之前遍歷。
2)可以觀察到在中序遍歷中,7是第4個值(從0開始算起)。由於中序遍歷順序為:左子樹,根結點,右子樹。所以7左邊的{4,10,3,1} 這四個結點屬於左子樹,而根結點7右邊的{11,8,2}屬於右子樹。
3)可以從上面的結論很輕鬆的得到遞迴式。在構建了根結點7後,我們可以根據中序遍歷{4,10,3,1} 和{11,8,2}分別構建它的左子樹和右子樹。我們同時需要相應的先序遍歷結果用於發現規律。我們可以由先序遍歷知道左右子樹的先序遍歷分別是{10,4,3,1}和{2, 8, 11}。左右子樹也分別為二叉樹,由此可以遞迴來解決問題。
4)關於如何得到根結點在中序遍歷中的位置問題還沒有細講,如果使用線性掃描查詢位置,則每次查詢需要O(N)的時間,如果二叉樹平衡的話,則整個演算法需要O(NlgN)的時間。如果二叉樹不平衡,則最壞情況需要O(N^2)時間。為了提高效率,我們可以考慮使用雜湊表來儲存與查詢根結點在中序遍歷中的位置,每次查詢只需要O(1)的時間,這樣構建整棵樹只需要O(N)的時間。 這裡為了方便,只是用了一個數組用於標記位置,要是用雜湊表也會很方便。需要注意的是,這裡的二叉樹結點值不能有相同的值。
程式碼:
// 輸入:先序和中序的第一個指標和最後一個指標,
// 遞迴呼叫,每次確定當前結點
BinaryTreeNode* ConstructCore(int* startPerorder, int* endPreorder, int* startInorder, int* endInorder)
{
//先序第一個為根節點
int rootValue = startPerorder[0];
BinaryTreeNode* root = new BinaryTreeNode;
root->m_nValue = rootValue;
root->m_pLeft = root->m_pRight = NULL;
//遞迴退出條件
if ( startPerorder==endPreorder )
{
if ( startInorder==endInorder && *startPerorder==*endInorder )
return root;
else
throw std::exception("Invalid input."); //異常處理
}
// 在中序遍歷中找到根節點的值
int* rootInorder = startInorder;
while(rootInorder<=endInorder && *rootInorder!=rootValue)
++rootInorder;
//異常處理
if ( rootInorder==endInorder && *rootInorder!=rootValue)
throw std::exception("Invalid input.");
int leftLength = rootInorder - startInorder;
int* leftPreorderEnd = startPerorder+leftLength;
if ( leftLength > 0 )
{
//構建左子樹
root->m_pLeft=ConstructCore(startPerorder+1,leftPreorderEnd,startInorder, rootInorder-1);
}
if ( leftLength < endPreorder-startPerorder )
{
//構建右子樹
root->m_pRight= ConstructCore(leftPreorderEnd+1,endPreorder,rootInorder+1,endInorder);
}
return root;
}
//根據先序和中序構建二叉樹
BinaryTreeNode* Construct(int* preorder, int* inorder, int length)
{
if(preorder==NULL || inorder==NULL || length <=0)
return NULL;
return ConstructCore(preorder, preorder+length-1, inorder,inorder+length-1);
}
void TraverseTreePostorder(BinaryTreeNode* proot)
{
if ( proot == NULL )
return;
if ( proot->m_pLeft != NULL )
TraverseTreePostorder(proot->m_pLeft);
if ( proot->m_pRight != NULL )
TraverseTreePostorder(proot->m_pRight);
cout << proot->m_nValue << " ";
}
主函式測試程式碼:
int main()
{
int preorder[] = {7,10,4,3,1,2,8,11};
int inorder[] = {4,10,3,1,7,11,8,2};
BinaryTreeNode* pRoot = Construct(preorder,inorder,8);
TraverseTreePostorder(pRoot);
cout <<endl;
return 0;
}
返回結果如下圖所示:
2)根據中序遍歷序列和後序序遍歷序列構建二叉樹
原理和上述原理基本一致,任然沿用上述例子
___7___ / \ 10 2 / \ / 4 3 8 \ / 1 11
那麼它的中序遍歷和後序序遍歷的結果如下:
inorder = {4,10,3,1,7,11,8,2}
postorder = {7,10,4,3,1,2,8,11}
程式碼:
// 輸入:中序和後序的第一個指標和最後一個指標,
// 遞迴呼叫,每次確定當前結點
BinaryTreeNode* ConstructCore_in_post(int* startInorder, int* endInorder, int* startPostorder, int* endPostorder)
{
//後序最後一個結點為根結點
int rootValue = *endPostorder;
BinaryTreeNode* root = new BinaryTreeNode;
root->m_nValue = rootValue;
root->m_pLeft = root->m_pRight = NULL;
//遞迴退出條件
if ( startInorder==endInorder )
{
if ( startPostorder==endPostorder && *startInorder==*endPostorder )
return root;
else
throw std::exception("Invalid input."); //異常處理
}
//在中序中找到當前根節點
int* rootInorder = startInorder;
while(rootInorder<=endInorder && *rootInorder != rootValue )
++rootInorder;
int leftLength = rootInorder - startInorder;
int* leftInorderEnd = startInorder+leftLength-1;
if ( leftLength > 0 )
{
//構建左子樹
root->m_pLeft=ConstructCore_in_post(startInorder,leftInorderEnd,startPostorder, startPostorder+leftLength-1);
}
if ( leftLength < endInorder-startInorder )
{
//構建右子樹
root->m_pRight= ConstructCore_in_post(rootInorder+1,endInorder,startPostorder+leftLength,endPostorder-1);
}
return root;
}
//根據先序和中序構建二叉樹
BinaryTreeNode* Construct(int* inorder, int* postorder, int length)
{
if(inorder==NULL || postorder==NULL || length <=0)
return NULL;
return ConstructCore_in_post(inorder, inorder+length-1, postorder, postorder+length-1);
}
void TraverseTreePreorder(BinaryTreeNode* proot)
{
if ( proot == NULL )
return;
cout << proot->m_nValue << " ";
if ( proot->m_pLeft != NULL )
TraverseTreePreorder(proot->m_pLeft);
if ( proot->m_pRight != NULL )
TraverseTreePreorder(proot->m_pRight);
}
主函式測試:
int main()
{
int inorder[] = {4,10,3,1,7,11,8,2};
int postorder[] = {4,1,3,10,11,8,2,7};
BinaryTreeNode* pRoot = Construct(inorder,postorder,8);
TraverseTreePreorder(pRoot);
cout <<endl;
return 0;
}
返回結果如下圖: