數據結構(15.2)樹與二叉樹

前言

樹形結構是非線性數據結構的一種,在現實生活中有着廣泛應用。其中,又以二叉樹的應用最爲常見。

二叉樹是樹型結構的一種,它規定每個結點至多隻有兩個子樹,並且子樹有左右之分,不能調換次序。

這意味着,二叉樹只有五種基本的形態。

img_1

具體的概念和性質就不詳細說了,下面說二叉樹的存儲結構。

二叉樹的存儲結構

如果使用順序存儲結構來存儲二叉樹,首先要約定存儲順序是從上至下、從左至右來存儲每一個結點。假如結點不存在,則用特殊的標記(如0),來表示。

可以發現,結點即使不存在,也需要耗費空間去記錄這個“不存在”的狀態。很顯然順序存儲結構在存儲完全二叉樹時纔有比較好的性能;拿來存儲一般的二叉樹則容易造成空間上的浪費。因此通常使用鏈式存儲結構來存儲二叉樹。

又到了熟悉的問題:結點如何設計?同往常的鏈式存儲一樣,結點包括了數據域和指針域兩個部分。不同的是,指針域有兩個指針,分別指向左右孩子結點。

//結點
typedef struct BinTreeNode{
    //數據域
    ElemType data;
    //左孩子
    struct BinTreeNode *leftChild;
    //右孩子
    struct BinTreeNode *rightChild;
}BinTreeNode;

對整棵二叉樹來說,記錄了根結點,就能找到所有的結點。因此,整棵樹的結構裏,只需要記錄一個根結點就可以了。

但是,我們需要通過輸入字符串來生成一棵二叉樹,要有一個特殊的標記值來表示結點不存在,以便生成正確的二叉樹。因此,表示整個二叉樹的結點,除了要記錄根結點以外,還要記錄標記值。

//整棵樹
typedef struct BinTree{
    //根結點
    BinTreeNode *root;
    //停止的標記
    ElemType refvalue;
}BinTree;

二叉樹的初始化

初始化二叉樹,就是將根結點初始化爲空,同時設置結束標記(結束標記由用戶設置,在參數中傳入)

//初始化二叉樹
void InitBinTree(BinTree *bt, ElemType ref){
    //將樹根設置爲空
    bt->root = NULL;
    //設置結束標記
    bt->refvalue = ref;
}

二叉樹的創建

這裏講的是將輸入的二叉樹字符串用二叉樹進行存儲。爲了表示左右子樹爲空,還需要特殊的標記值來標記,這樣才能生成一個正確的二叉樹。

img_2

輸入的字符串是先序序列,那麼創建結點的順序自然是:創建根結點->創建左子樹->創建右子樹。

也就是說,讀取輸入的字符,若是結束標記,則指針域爲空,退出創建;否則創建結點、保存數據,再遞歸創建左右子樹即可

//輸入的字符串:ABC##DE##F##G#H##
void CreateBinTree(BinTree *bt, BinTreeNode **t){
    ElemType item;
    scanf("%c",&item);
    
    if (item == bt->refvalue) {
        (*t) = NULL;
    }else{
        //創建結點
        (*t) = (BinTreeNode *)malloc(sizeof(BinTreeNode));
        assert((*t) != NULL);
        
        (*t)->data = item;
        CreateBinTree(bt, &(*t)->leftChild);
        CreateBinTree(bt, &(*t)->rightChild);
    }
}

二叉樹的遍歷

二叉樹的遍歷在上篇文章中寫得很詳細了,見數據結構(15.1)二叉樹的遍歷及其七種實現方式

二叉樹的恢復

二叉樹的恢復指的是,已知二叉樹的先序遍歷和中序遍歷,或者中序遍歷和後序遍歷,將它恢復成一棵完整的二叉樹。也就是給定兩個已知序列的字符串,創建出一個二叉樹。

注:必須有一個序列是中序序列,否則是無法恢復的。

已知先序遍歷和中序遍歷

先看已知先序遍歷和中序遍歷序列的情況。

中序遍歷的順序是,先訪問左子樹,再訪問根結點,最後訪問右子樹。因此在中序序列中,位於根結點字符左邊的所有字符,在樹中也一定在根結點左側;位於根結點字符右邊的所有字符,在樹中也一定在根結點右側。

而通過先序序列,我們正好可以得到根結點,然後利用中序序列劃分出結點的左側子樹和右側子樹。一個結點一個結點進行劃分,最終就可以恢復整棵樹。

img_3

  1. 在先序序列中讀取到根結點 A,在中序序列中找到A所在的位置,可以看出A的左側子樹和右側子樹

  2. 在先序序列中讀取到結點B,在中序序列中發現它在A的左側,可得B爲A的左子樹。同時,B也是根結點,通過中序序列可以看出B的左側子樹和右側子樹。

  3. 在先序序列中讀取到結點C,在中序序列中發現它在B的左側,可得C爲B的左子樹。同時,C也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

img_4

  1. 在先序序列中讀取到結點D,在中序序列中發現它在B的右側,可得D爲B的右子樹。同時,D也是根結點,通過中序序列可以看出D的左側子樹和右側子樹。(雖然D也在A的左側,但是A的左子樹已經是B了,以下同理)

  2. 在先序序列中讀取到結點E,在中序序列中發現它在D的左側,可得E爲D的左子樹。同時,E也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

  3. 在先序序列中讀取到結點F,在中序序列中發現它在D的右側,可得F爲D的右子樹。同時,D也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

img_5

  1. 在先序序列中讀取到結點G,在中序序列中發現它在A的右側,可得G爲A的右子樹。同時,G也是根結點,通過中序序列可以看出G的左側子樹(空)和右側子樹。

  2. 在先序序列中讀取到結點H,在中序序列中發現它在G的右側,可得H爲G的右子樹。同時,H也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

已知後序遍歷和中序遍歷

通過後序遍歷,我們也能得到根結點,然後同樣利用中序序列劃分出結點的左側子樹和右側子樹,最終也可以恢復整棵樹。

img_6

  1. 在後序序列中讀取到根結點 A,在中序序列中找到A所在的位置,可以看出A的左側子樹和右側子樹。

  2. 在後序序列中讀取到結點G,在中序序列中發現它在A的右側,可得G爲A的右子樹。同時,G也是根結點,通過中序序列可以看出G的左側子樹(空)和右側子樹。

  3. 在後序序列中讀取到結點H,在中序序列中發現它在G的右側,可得H爲G的右子樹。同時,H也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

img_7
4. 在後序序列中讀取到結點B,在中序序列中發現它在A的左側,可得B爲A的左子樹。同時,B也是根結點,通過中序序列可以看出B的左側子樹和右側子樹。

  1. 在後序序列中讀取到結點D,在中序序列中發現它在B的右側,可得D爲B的右子樹。同時,D也是根結點,通過中序序列可以看出D的左側子樹和右側子樹。

img_8

  1. 在後序序列中讀取到結點F,在中序序列中發現它在D的右側,可得F爲D的右子樹。同時,D也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

  2. 在後序序列中讀取到結點E,在中序序列中發現它在D的左側,可得E爲D的左子樹。同時,E也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

  3. 在後序序列中讀取到結點C,在中序序列中發現它在B的左側,可得C爲B的左子樹。同時,C也是根結點,但是在中序序列中它左右側都沒有未確定的字符了,因此左右子樹爲空。

通過以上的情況,不難想到爲什麼沒有中序序列將無法恢復整棵樹:我們需要靠中序序列判斷結點是屬於左子樹還是右子樹,只有先序序列和後序序列是無法判斷的。

例如,已知先序序列ABC,和後序序列CBA,雖然可以判斷根結點A,但是無法判斷BC哪個是左子樹哪個是右子樹,可能會有如下狀況:

img_9

全部代碼

這部分代碼比較多,還在整理中,整理完了上傳。

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