這一小節我們介紹一種特殊的樹—二叉樹。它的特殊之處在於每個?根節點的葉子節點個數不超過兩個,所以我們很容想到通過下面的結構定義二叉樹:
typedef char ElemType;typedef struct BTreeNode{ElemType vaule;struct BTreeNode *Lchild;struct BTreeNode *Rchild;}BTreeNode,*BTree;
二叉樹的核心操作也是遍曆,根據訪問節點值與訪問左右孩子的先後順序的不同,有3種遍曆方式:前序走訪、中序遍曆和後序遍曆。前序走訪就是先訪問節點值,再訪問左孩子,最後訪問右孩子;中序遍曆就是先訪問左孩子,再訪問節點值,最後訪問右孩子;後續遍曆是先訪問左孩子,再訪問右孩子,最後訪問節點值。
//初始化一個節點BTree initNode(ElemType e){BTree pnew = (BTree)malloc(sizeof(BTreeNode));pnew->vaule = e;pnew->Lchild = NULL;pnew->Rchild = NULL;return pnew;}//釋放二叉樹:採用後續遍曆的方式比較容易void destroyBTree(BTree *T){if(NULL == *T)return;destroyBTree(&(*T)->Lchild);destroyBTree(&(*T)->Rchild);free(*T);*T = NULL;}//前序走訪void PreOrderTraverse(BTree T){if(NULL == T)return;else{printf("%c",T->vaule);PreOrderTraverse(T->Lchild);PreOrderTraverse(T->Rchild);}}//中序遍曆void InOrderTraverse(BTree T){if(NULL == T)return;else{InOrderTraverse(T->Lchild);printf("%c",T->vaule);InOrderTraverse(T->Rchild);}}//後序遍曆void PostOrderTraverse(BTree T){if(NULL == T)return;else{PostOrderTraverse(T->Lchild);PostOrderTraverse(T->Rchild);printf("%c",T->vaule);}}//求二叉樹樹的深度int TreeHeight(BTree T){int leftlen = 0;int rightlen = 0;int currMaxLen = 0;int len = 0;if(NULL == T)return 0;else{leftlen = TreeHeight(T->Lchild);rightlen = TreeHeight(T->Rchild);//擷取左右子樹高度的最大值leftlen>rightlen?currMaxLen = leftlen:currMaxLen = rightlen;//與之前記錄的最大值相比if(len < currMaxLen)len = currMaxLen;}return len+1;}//求二叉樹中全部節點的個數int count(BTree T){int leftCount = 0;int rightCount = 0;if(NULL == T )return 0;if(T){leftCount = count(T->Lchild);rightCount = count(T->Rchild);}return leftCount+rightCount+1;}//樹的尋找//通過全域變數將尋找到的結過帶出來BTree result = NULL;void locateElem(BTree T,ElemType e){if(NULL == T)return ;//如果找到,把它賦給全域變數if(T->vaule == e){result = T;}if(T!= NULL){locateElem(T->Lchild,e);locateElem(T->Rchild,e);}}BTree findLeftChild(BTree T,ElemType e){//必須清空全域變數result = NULL;locateElem(T,e);if(NULL == result )return NULL;elsereturn result->Lchild;}BTree findRightChild(BTree T,ElemType e){result = NULL;locateElem(T,e);if(NULL == result )return NULL;elsereturn result->Rchild;}//插入子樹//參數為:插入的某個數,插入節點的位置(0代表左子樹,1代表右子樹),插入為這個節點的左子樹或者右子樹,待插入的子樹bool insertSubTree(BTree T,ElemType e,int LR,BTree subT){//遍曆樹,找到插入的位置result = NULL;locateElem(T,e);//判斷是否可以插入if(0 == LR && NULL == result->Lchild ){result->Lchild = subT;return true;}if(1 == LR && NULL == result->Rchild){result->Rchild = subT;return true;}return false;}//刪除子樹//參數為:需要被刪除的樹,刪除的位置,刪除它的左子樹還是右子樹bool deleteSubTree(BTree T,ElemType e,int LR){//遍曆樹,找到待的位置result = NULL;locateElem(T,e);//判斷是否可以刪除if(0 == LR && NULL != result->Lchild ){destroyBTree(&result->Lchild);return true;}if(1 == LR && NULL != result->Rchild){destroyBTree(&result->Rchild);return true;}return false;}
跟樹的程式類似,我們必須自己將樹中的關係串連起來。其實建立二叉樹也可以類的利用遞迴實現:
void Create(BTree &root ) { printf("Enter the data \n");char tmp;scanf(" %c",&tmp); if(tmp =='#'){root = NULL;}else{ root =(BiNode*) malloc(sizeof(BiNode)); root->data = tmp;Create(root->lch); Create(root->rch);} }
但是我更喜歡“自由”的建立它,所以並沒有那樣寫函數。
所謂樹的先序、中序、後序遍曆,是通過將訪問函數寫在兩次遞迴的前面、中間和後面實現的。注意到,刪除節點時的特點,必須要將這個節點的子樹刪除,才能刪除節點本身,所以採用後序遍曆會很方便,不用在像刪除鏈表的節點那樣,必須記錄被刪除節點的前一個位置。跟二叉樹尋找的有關操作要注意的是,程式是通過全域變數將尋找到的節點帶出來,所以如果再做跟尋找有關的操作,必須先將全域變數初始化。
很多書籍對樹避而不談的原因,是因為對樹可以轉換成二叉樹,而二叉樹的操作更加簡單、規範。只要把我們之前定義的樹的兄弟部分順時針旋轉45度,看起來他就是一顆二叉樹了。其實,因為在我們定義時,只通過了孩子、兄弟兩根指標定義樹,所以這種定義方法也稱為二叉樹定義法。
還有兩種特殊的二叉樹需要專門提一下:滿二叉樹和完全二叉樹。
滿二叉樹就是每一層的葉子節點都是滿的,而完全二叉樹則是一棵這樣的樹:如何把它的所有n個節點從上到下,從左至右依次編號,那麼與對應的完全二叉樹是相同的。也就是說,完全二叉樹像是把滿二叉樹的最後若干的元素去掉。這類二叉樹有一個特點:就是按照節點序號可以確定節點之間的父子關係:編號為i的節點的兩個子節點序號是2*i和2*i+1;而編號為i的節點的父節點序號是i/2(整除)。所以可以通過數組來儲存這類二叉樹,這樣它的操作會很方便。對於其他的二叉樹,可以通過給那些缺失的節點補一個特殊的值來轉化成完全二叉樹。當然,如果補的節點太多,就得不償失了。