二叉樹常用操作演算法集、解釋及注意事項
阿新 • • 發佈:2019-01-26
二叉樹是一種常用的資料結構,在程式中也經常需要使用二叉樹,但是你所使用語言卻並不一定提供了二叉樹這種資料型別,所以為了方便使用,我們可以自己實現一個二叉樹的資料型別。在需要時就像使用其他已定義的型別一樣方便。
下面給出一些本人寫的演算法和解釋(基於C語言),希望對讀者寫一個二叉樹資料型別有所幫助。
0、遞迴的四條基本法則
由於二叉樹中的演算法大多使用遞迴來實現,而且使用遞迴實現也使程式碼非常簡潔和易於理解。但是寫一個好的遞迴演算法並不是一件容易的事,所以我覺得在開始這些演算法的講解之前有必要向大家說說遞迴實現的一些法則。而且本文中的程式碼都是以下面的法則作為依據的(至少我是這樣認為)。
1)基準情形。必須總有某些基準情形,它無需遞迴就能解出。
2)不斷推進。對於那些需要遞迴的情形,每一次遞迴呼叫都必須要使求解狀況朝接近基準情形的方向推進。
3)設計法則。假設所有的遞迴呼叫都能進行。
4)合成效益法則。在求解一個問題的同一例項時,切勿在不同的遞迴呼叫中做重複性的工作。
1、資料的儲存結構和定義
#define TRUE 1
#define FALSE 0
//定義自己的資料型別
typedef char DataType;
typedef int BOOL;
typedef struct BiNode
{
DataType cData; //用於儲存真正的資料
struct BiNode *LChild;//指向左孩子
struct BiNode *RChild;//指向右孩子
}BiNode, *BiTree;
2、基本操作的實現
1)遍歷
遍歷二叉樹是其他操作的基礎,二叉樹的很多操作都是建立在遍歷的基礎上的,掌握了遍歷對其他演算法的理解和實現都大有幫助,那麼我們就先來看一看遍歷的演算法,在二叉樹中,根據訪問根的次序分為3種,即先序遍歷(先訪問根,再先序訪問左子樹,最後先序訪問右子樹),中序遍歷(先中序訪問左子樹,訪問根,最後中序訪問右子樹)和後序遍歷(先後序訪問左子樹,再後序訪問右子樹,最後訪問根),還有一種就是層次性遍歷(藉助佇列進行),它們的實現如下:
說明:從上面的程式碼我們可以看到,如果在函式中除去結點的訪問,則先序、中序和後序的遍歷程式碼是完全一樣。可見這三種次序的遍歷僅在訪問根的次序上存在差異。 2)銷燬以BT為根結點的樹BOOL PreOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*)) { //先序遍歷二叉樹,對每個結點呼叫Visit一次,且僅一次 //實現對結點的某種操作,Visit失敗,則遍歷失敗 if(BT != NULL) { if((*Visit)(BT))//訪問根結點 { if(PreOrderTraverse(BT->LChild, Visit))//先序訪問左子樹 if(PreOrderTraverse(BT->RChild, Visit))//先序訪問右子樹 return TRUE; return FALSE; } } else return TRUE; } //---------------------------------------------------------------------- BOOL InOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*)) { //中序遍歷二叉樹,對每個結點呼叫Visit一次,且僅一次 //實現對結點的某種操作,Visit失敗,則遍歷失敗 if(BT != NULL) { if(InOrderTraverse(BT->LChild, Visit))//中序訪問左子樹 { if((*Visit)(BT))//訪問根結點 if(InOrderTraverse(BT->RChild, Visit))//中序訪問右子樹 return TRUE; return FALSE; } } else return TRUE; } //---------------------------------------------------------------------- BOOL PostOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*)) { //後序遍歷二叉樹,對每個結點呼叫Visit一次,且僅一次 //實現對結點的某種操作,Visit失敗,則遍歷失敗 if(BT != NULL) { if(PostOrderTraverse(BT->LChild, Visit))//後序訪問左子樹 { if(PostOrderTraverse(BT->RChild, Visit))//後序訪問右子樹 if((*Visit)(BT))//訪問根結點 return TRUE; return FALSE; } } else return TRUE; } //---------------------------------------------------------------------- BOOL LevelOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*)) { //層次性遍歷二叉樹,對每個結點呼叫Visit一次,且僅一次 //實現對結點的某種操作,Visit失敗,則遍歷失敗 //使用陣列模擬一個迴圈佇列 if(BT == NULL) return TRUE; const int nCapicity = 300; BiTree DT[nCapicity]; int nFront = 0, nRear = 1; DT[0] = BT; //根結點入隊 int nSize = 1; while(nSize != 0) //佇列非空 { if(DT[nFront]->LChild) { //左子樹非空,左子樹入隊 DT[nRear] = DT[nFront]->LChild; ++nRear; ++nSize; } if(DT[nFront]->RChild) { //右子樹非空,右子樹入隊 DT[nRear] = DT[nFront]->RChild; ++nRear; ++nSize; } //訪問隊頭元素,並出隊 if(!(*Visit)(DT[nFront])) return FALSE; ++nFront; --nSize; if(nSize > nCapicity) return FALSE; if(nRear == nCapicity) nRear = 0; if(nFront == nCapicity) nFront = 0; } return TRUE; }
說明:本人認為銷燬操作以後序來銷燬比較好,因為它是最為直觀的做法,因為如果採用先序來銷燬,則需要兩個變數來儲存BT的左孩子(BT->LChild)和右孩子(BT->RChild),因為先銷燬根,即free(BT)後,就不能再利用BT卻直接引用其左孩子或右孩子,即不能使用這樣的語句:DestoryBiTree(BT->LChild);DestoryBiTree(BT->RChild);。同樣的道理,中序銷燬需要一個變數來儲存BT的右子樹。 此外,此演算法可用於銷燬整棵樹或樹的任意子樹,只要BT是所要刪除的樹的根的指標即可。 3)查詢二叉樹中結點值為c的結點BiTree DestoryBiTree(BiTree BT) { //釋放所有的樹結點,並把指向樹根的指標置空 //只能用後序free,否則需要1個(中序)或2個(前序)臨時變數 //來儲存BT->LChild和BT->RChild if(BT) { DestoryBiTree(BT->LChild); DestoryBiTree(BT->RChild); free(BT); } return NULL; }
BiTree FindNode(BiTree BT, DataType c)
{
//返回二叉樹BT中值為c的結點的指標
//若c不存在於BT中,則返回NULL
if(!BT)
return NULL;
else if(BT->cData == c) //找到相應的結點,返回其指標
return BT;
BiTree BN = NULL;
BN = FindNode(BT->LChild, c); //在其左子樹中進行查詢
if(BN == NULL) //沒有找到,則繼續在其右子樹中進行查詢
BN = FindNode(BT->RChild, c);
return BN;
}
說明:查詢操作可選用先序、中序和後序查詢中的任一種都可,這裡採用的是先序的查詢。此外如果你所用的語言支援引用型別,函式的定義變為BiTree FindNode(BiTree BT, const DataType &c)效率會更佳,由於C語言沒有引用型別,所以只能寫成上面的樣子了。
4)求以BT為根結點的二叉樹深度
int BiTreeDepth(BiTree BT)
{
//求樹的深度
//從二叉樹深度的定義可知,二叉樹的深度應為其左、右子樹深度的最大值加1,
//因為根結點也算1層。
if(BT == NULL) //若為空樹,則返回-1
return -1;
else
{
int nLDepth = BiTreeDepth(BT->LChild); //求左樹的深度
int nRDepth = BiTreeDepth(BT->RChild); //求右樹的深度
if(nLDepth >= nRDepth)
{
return nLDepth+1;
}
else
{
return nRDepth+1;
}
}
}
說明:有些書上認為空樹的深度為0,只有一個結點的二叉樹的深度為1,但是這裡我採用空樹的深度為-1,只有一個結點的二叉樹的深度為0的做法。
5)求二叉樹中某結點的雙親結點
BiTree GetParent(BiTree BT, DataType c)
{
//獲得值為c的結點的雙親結點,
//若c為根結點或不存在於樹中,則返回NULL
if(!BT || BT->cData == c)
return NULL;
if((BT->LChild && BT->LChild->cData == c) ||
(BT->RChild && BT->RChild->cData == c))
return BT;
BiTree Parent = NULL;
Parent = GetParent(BT->LChild, c);
if(Parent == NULL)
Parent = GetParent(BT->RChild, c);
return Parent;
}
說明:在判斷其左孩子或右孩子的值前,首先要判斷其左孩子或右孩子是否為空,例如,若BT的左子樹為空,則表示式BT->LChild->cData這樣的語句是會產生異常的,所以在判等之前一定要檢查其孩子是否為空。
此外,函式返回NULL意味著有兩種可能的情況,一是此結點為樹的根結點(根結點沒有雙親結點),二是這個結點不存在於樹中。所以在應用時,如果檢測到返回值為NULL則還要判斷值為c的結點是否是根結點,若它不是根結點,則表示在樹BT中不存在值為c結點。
與查詢同樣的道理,如果你所用的語言支援引用型別,函式的定義變為BiTree GetParent (BiTree BT, const DataType &c)效率會更佳。
6)找出二叉樹中的最大、最小值
BiTree MaxNode(BiTree BT)
{
//返回二叉樹BT中結點的最大值
if(BT == NULL) //空樹則返回NULL
return NULL;
BiNode *pMax = BT; //預設以樹根作為當前最大結點
BiNode *tmp = MaxNode(BT->LChild); //找出左子樹的最大結點
if(tmp != NULL)
{
//左子樹存在,且左子對的最大結點大於當前最大結點
if(tmp->cData > pMax->cData)
pMax = tmp;
}
tmp = MaxNode(BT->RChild); //找出右子樹的最大結點
if(tmp != NULL)
{
//右子樹存在,且右子樹的最大結點大於當前最大結點
if(tmp->cData > pMax->cData)
pMax = tmp;
}
return pMax;
}
說明:找出最小結點的演算法思想實現與此相同,在這裡不再給出。這個演算法主要要注意的就是左子樹或右子樹是否存在,以免因為訪問記憶體的錯誤而讓程式發生異常。因為左子對不存在時,根據程式碼可知它會返回NULL,則不能對其進行引用,即不能使用tmp->cData之類的語句。
7)求二叉樹中的葉子結點和非葉子結點的個數
int LeavesCount(BiTree BT)
{
//返回二叉樹BT中葉子結點的個數
if(BT == NULL)
return 0; //BT為空樹,返回0
int nCount = 0;
if(!(BT->LChild || BT->RChild))
++nCount; //BT為葉子結點,加1
else
{
//累加上左子樹上的葉子結點
nCount += LeavesCount(BT->LChild);
//累加上右子樹上的葉子結點
nCount += LeavesCount(BT->RChild);
}
return nCount;
}
//----------------------------------------------------------------------
int NotLeavesCount(BiTree BT)
{
//返回二叉樹BT中非葉子結點的個數
if(BT == NULL || (!(BT->LChild || BT->RChild)))
return 0; //若為BT為空樹或為葉子結點,返回0
else
{
int nCount = 1; //此時根結點也是一個非葉子結點
//累加上左子樹的非葉子結點個數
nCount += NotLeavesCount(BT->LChild);
//累加上右子樹的非葉子結點個數
nCount += NotLeavesCount(BT->RChild);
return nCount;
}
}
說明:表示式:!(BT->LChild || BT->RChild)為判斷一個結點是否為葉子結點,若為葉子結點,則值為真,否則為假。
3、補充
1)所有的演算法中,對樹的引數的傳遞都為傳遞所需要的樹的根結點的指標,介面較為統一,使用方便簡單,不易出錯。
2)可對DataType進行重新定義來完全複用這些演算法。
3)這些操作都是二叉樹中很基本的操作,可通過這些操作組合出更多的功能和操作,這些函式本人通過簡單的測試,沒有發現執行錯誤。
如發現演算法有錯誤,請各位讀者指出!