中序遍歷和先序遍歷/後序遍歷構建二叉樹

1、問題

給定二叉樹的2個遍歷序列(如先序+中序,先序+後序,中序+後序等),是否能夠根據這2個遍歷序列唯一確定二叉樹?


2、理論分析

數據結構的基礎知識中重要的一點就是能否根據兩種不同遍歷序列的組合(有三種:先序+中序,先序+後序,中序+後序),唯一的確定一棵二叉樹。然後就是根據二叉樹的不同遍歷序列(先序、中序、後序),重構二叉樹。顯然,這三種組合並不是都能唯一確定二叉樹的,其中先序+後序就不能唯一確定一棵二叉樹,下面是關於該問題的證明與結論。

給定二叉樹結點的前序序列和中序序列,可以唯一確定該二叉樹
證明:因爲先序序列的第一個元素是根結點,該元素將二叉樹中序序列分成兩部分,左邊(假設有L個元素)表示左子樹,若左邊無元素,則說明左子樹爲空;右邊(假設有R個元素)是右子樹,若爲空,則右子樹爲空。根據前序遍歷中
"根-左子樹-右子樹"
的順序,則由從先序序列的第二元素開始的L個結點序列和中序序列根左邊的L個結點序列構造左子樹,由先序序列最後R個元素序列與中序序列根右邊的R個元素序列構造右子樹。

②由中序序列和先序序列能唯一確定一棵二叉樹,但是由先序序列和後序序列不能唯一確定一棵二叉樹,因無法確定左右子樹兩部分。
反例:任何結點只有左子樹的二叉樹和任何結點只有右子樹的二叉樹,其前序序列相同,後序序列相同,但卻是兩棵不同的二叉樹。
如: 2           2
    /            \
   1              1
  /                \
 3                  3
這兩棵二叉樹的先序遍歷序列都爲2-1-3,後序遍歷序列都爲3-1-2。但是顯然它們是不同的二叉樹,所以根據先序序列和後序序列並不能唯一確定二叉樹。

③已經說明由二叉樹的先序序列和中序序列可以確定一棵二叉樹,現在來證明由二叉樹的中序序列和後序序列,也可以唯一確定一棵二叉樹。

證明:
當n
=
1時,只有一個根結點,由中序序列和後序序列可以確定這棵二叉樹。
設當n
=m-1時結論成立,即結點數目爲m-1時,中序序列和後序序列可以唯一確定二叉樹。現證明當n=
m時結論成立。
設中序序列爲S1,S2,…,Sm,後序序列是P1,P2,…,Pm。因後序序列最後一個元素Pm是根,則在中序序列中可找到與Pm相等的結點(設二叉樹中各結點互不相同)Si(
1≤i≤m),因中序序列是由中序遍歷而得,所以Si是根結點,S1,S2,…,Si-1是左子樹的中序序列,而Si+1,Si+2
,…,Sm是右子樹的中序序列。
若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)的時間。 這裏爲了方便,只是用了一個數組用於標記位置,要是用哈希表也會很方便。需要注意的是,這裏的二叉樹結點值不能有相同的值。

  1. int mapIndex[256];  
  2. void mapToIndices(int inorder[], int n)  
  3. {  
  4.     int i;  
  5.     for (i=0; i<n; i++) {  
  6.         mapIndex[inorder[i]] = i;  
  7.     }  
  8. }  
  9. //在這之前要調用mapToIndices方法。pre數組爲先序遍歷序列,注意在遞歸過程中pre起始位置是變化的。n爲結點數目,offset爲子樹開始位置。   
  10. struct node* buildInorderPreorder(int pre[],  
  11.         int n, int offset)  
  12. {  
  13.     if (n == 0) return NULL;  
  14.     int rootVal = pre[0];  
  15.     int i = mapIndex[rootVal] - offset;  
  16.     struct node* root = newNode(rootVal);  
  17.     root->left = buildInorderPreorder(pre+1,  
  18.             i, offset);  
  19.     root->right = buildInorderPreorder(pre+i+1,  
  20.             n-i-1, offset+i+1);  
  21.     return root;  
  22. }  
  23. //測試代碼   
  24. void buildInorderPreorderTest()  
  25. {  
  26.     int pre[] = {7, 10, 4, 3, 1, 2, 8, 11};  
  27.     int in[] = {4, 10, 3, 1, 7, 11, 8, 2};  
  28.     int n = sizeof(in) / sizeof(in[0]);  
  29.     int offset = 0;  
  30.     mapToIndices(in, n);  
  31.     struct node* root = buildInorderPreorder(pre, n, offset);  
  32.     traverse(root);  
  33.     putchar('\n');  
  34. }  
int mapIndex[256];
void mapToIndices(int inorder[], int n)
{
    int i;
    for (i=0; i<n; i++) {
        mapIndex[inorder[i]] = i;
    }
}
//在這之前要調用mapToIndices方法。pre數組爲先序遍歷序列,注意在遞歸過程中pre起始位置是變化的。n爲結點數目,offset爲子樹開始位置。
struct node* buildInorderPreorder(int pre[],
        int n, int offset)
{
    if (n == 0) return NULL;
    int rootVal = pre[0];
    int i = mapIndex[rootVal] - offset;
    struct node* root = newNode(rootVal);
    root->left = buildInorderPreorder(pre+1,
            i, offset);
    root->right = buildInorderPreorder(pre+i+1,
            n-i-1, offset+i+1);
    return root;
}
//測試代碼
void buildInorderPreorderTest()
{
    int pre[] = {7, 10, 4, 3, 1, 2, 8, 11};
    int in[] = {4, 10, 3, 1, 7, 11, 8, 2};
    int n = sizeof(in) / sizeof(in[0]);
    int offset = 0;
    mapToIndices(in, n);
    struct node* root = buildInorderPreorder(pre, n, offset);
    traverse(root);
    putchar('\n');
}


2)根據後序遍歷和中序遍歷構建二叉樹

跟前面原理類似,代碼如下:

  1. struct node *buildInorderPostorder(int post[], int n, int offset)  
  2.  {  
  3.   assert(n >= 0);  
  4.   if (n == 0) return NULL;  
  5.   int rootVal = post[n-1];  
  6.   int i = mapIndex[rootVal]-offset;  // the divider's index   
  7.   struct node *root = newNode(rootVal);  
  8.   root->left = buildInorderPostorder(post, i, offset);  
  9.   root->right = buildInorderPostorder(post+i, n-i-1, offset+i+1);  
  10.   return root;  

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