1. 程式人生 > >二叉搜尋樹(BST)

二叉搜尋樹(BST)

什麼是二叉搜尋樹

        一顆二叉搜尋樹是以一顆二叉樹來組織的,這樣的一棵樹可以用一個連結串列資料結構來表示,其中每一個結點就是一個物件。除了key和衛星資料之外,每個結點還包括屬性left,right和p。它們分別指向結點的左孩子、右孩子和雙親。如果某個孩子結點和父節點不存在,則相應屬性的值為NIL。其中衛星資料是指:在實際中,待排序的數很少是單獨的數值,每個記錄包含一個關鍵字(key),就是排序問題中需要重排的值。記錄的其他部分有衛星資料組成,通常與關鍵字是一同存取的。

        二叉排序樹( Binary Sort Tree),又稱二叉搜尋樹。它或者是一顆空樹,或者是具有下列性質的二叉樹:

  • 若它的左子樹不為空,則左子樹上所有結點的值均小於它的根結點的值。
  • 若它的右子樹不為空,則右子樹上所有結點的值均大於它的根結點的值。
  • 它的左、右子樹也都為二叉排序樹。
        由前面介紹的順序查詢可知,插入和刪除的效率並不是十分理想,構造一顆二叉排序樹的目的,其實不是為了排序,而是為了提高查詢和插入刪除關鍵字的速度。二叉搜尋樹性質允許我們通過一個簡單的遞迴演算法來按序輸出二叉搜尋樹中的所有關鍵字,這種演算法稱為中序遍歷演算法(輸出的子樹根的關鍵字位於其左子樹的關鍵字值和右子樹關鍵字值之間)。但不管怎麼說,在一個有序資料集上的查詢,速度總是要快於無序的資料集的,而二叉排序樹這種非線性的結構,也有利於插入和刪除的實現。因此,對於一顆二叉搜尋樹,使用簡單的遞迴演算法即可輸出二叉搜尋樹T中的所有元素:         
        對於上面的演算法我們可知,如果x為一棵有n個結點子樹的根,那麼遍歷一棵有n個結點的二叉搜尋樹需要耗費O(n)的時間。(NIL表示指向一個物件的指標為空)

二叉搜尋樹的查詢

        首先,我們給出一個簡單的二叉樹的結構:

typedf struct BiTNode
{
    int data;//結點資料
    struct BiTNode *lchild, *rchild;//左右孩子指標
}BiTNode,*BiTree;
        在一棵二叉搜尋樹中查詢一個具有給定關鍵字的結點。輸入一個指向樹根的指標和一個關鍵字k,如果這個結點存在,函式返回一個指向關鍵字k的結點的指標,否則返回NIL。

        對此,我們可以給出類似的實現:

//遞迴查詢二叉排序樹T中是否存在key
//指標f指向T的雙親,其初始值為NULL
//若查詢成功,則指標p指向該資料元素結點,並返回TRUE
//否則指標p指向路徑上訪問的最後一個結點並返回FALSE
int SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
    if(!T)
    {
        *p = f;
        return false;
    }
    else if (key == T->data)
    {
        *p = T;
        retrun true;        
    }
    else if(key < T->data)
        return SearchBST(T->lchild,key,T,p);
    else
        return SearchBST(T->rchild,key,T,p);
}
        這個過程從樹根開始查詢,並沿著這棵樹的可行路徑向下進行,對遇到的每一個結點進行與關鍵字key的比較,根據比較結果的大小關係向下繼續查詢或結束查詢。其執行時間為O(h),h即為樹的高度。例如SearchBST(T,93,NULL,p),其中引數T為一個二叉連結串列,key為要查詢的關鍵字,二叉樹f指向T的雙親,當T指向根結點時,f的初值為NULL,它在遞迴時有用。最後的引數是為了查詢成功後可以得到查詢到的結點位置。同時,可以採用while迴圈來展開遞迴,用一種迭代方式重寫這個過程。

二叉搜尋樹的插入和刪除

        插入和刪除操作會引起二叉搜尋樹表示的動態集合的變化,一定要修改資料結構來反映這個變化。

二叉排序樹插入操作

int InsertBST(BiTree *T,int key)//當二叉排序樹不存在關鍵字等於key的資料元素時插入key
{
    BiTree p,s;
    if(!SearchBST(*T,key,NULL,&p))//p指向查詢路徑的中最後一個訪問的結點。
    {
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = key;
        s->lchild = s->rchild = NULL;
        if(!p)
            *T = s;//插入s為新的根結點
                                 //這裡表示結點為空時,將插入結點作為根結點。  
        else if (key < p->data)
            p->lchild = s;
        else
            p->rchild = s;
        return true;
    }
    else
        return false;
}

二叉排序樹刪除操作

        從一棵二叉搜尋樹T中刪除一個結點z的整個策略分為三種基本情況:

  • 葉子結點。即z沒有孩子結點,那麼只是簡單的將其刪除,並修改它的父結點,用NIL作為孩子結點來替代z。
  • 僅有左子樹或右子樹的結點。即z只有一個孩子,那麼將z刪除,並修改z的父結點,用z的孩子結點來替代z的位置。(將z的孩子提升到樹中z的位置上)
  • 左右子樹都有結點。即z有兩個孩子,那麼找z的後繼y(一定在z的右子樹中)或者前驅(在z的左子樹中),根據中序遍歷的性質可知,目標節點的直接前繼和直接後繼為當前結點左子樹最右的結點和右子樹下最左的結點。並讓y佔據樹中z的位置。z原來的右子樹成為y的新的右子樹,並且z的左子樹成為y的新的左子樹。(這裡還與y是否為z的右孩子有關)
        可以分為下面是四種情況:
  • 如果z沒有左孩子,那麼用其右孩子來替代z。這個右孩子可以是NIL,亦可以不是(即z為葉子結點的情況)如,如下圖a。
  • 如果z僅有一個孩子且只有一個左孩子,那麼就用左孩子來替代z,如下圖b。
  • z既有一個左孩子又有一個右孩子,我們要查詢z的後繼y,y位於z都右子樹中並且沒有左孩子。
    • 如果y是z的右孩子,那麼用y替代z,並僅留下y的右孩子,如下圖c。
    • 否則,y位於z的右子樹中但並不是z的右孩子,這種情況下,先用y的右孩子替換y,再用y替換z,如下圖d。
        

/* 若二叉排序樹T中存在關鍵字等於key的資料元素時,則刪除該資料元素結點, */
/* 並返回TRUE;否則返回FALSE。 */
int DeleteBST(BiTree *T,int key)
{ 
	if(!*T) /* 不存在關鍵字等於key的資料元素 */ 
		return FALSE;
	else
	{
		if (key==(*T)->data) /* 找到關鍵字等於key的資料元素 */ 
			return Delete(T);
		else if (key<(*T)->data)
			return DeleteBST(&(*T)->lchild,key);
		else
			return DeleteBST(&(*T)->rchild,key);
		 
	}
}
/* 從二叉排序樹中刪除結點p,並重接它的左或右子樹。 */
int Delete(BiTree *p)
{
	BiTree q,s;
	if((*p)->rchild==NULL) /* 右子樹空則只需重接它的左子樹(待刪結點是葉子也走此分支) */
	{
		q=*p; *p=(*p)->lchild; free(q);
	}
	else if((*p)->lchild==NULL) /* 只需重接它的右子樹 */
	{
		//*p=(*p)->lchild操作不僅能夠改變當前結點指標p的指向,同時在此函式修改形參的值,由於它的指標型別導致外層函式傳入的實參也被修改,
		//它的實參值實際為p的雙親結點指標域的值(存放的是子節點的地址)。即修改p也會導致刪除結點父結點的指標域中儲存的值的變化(即改變了指向)。
		q=*p; *p=(*p)->rchild; free(q);
	}
	else /* 左右子樹均不空 */
	{
		q=*p; s=(*p)->lchild;
		while(s->rchild) /* 轉左,然後向右到盡頭(找待刪結點的前驅) */
		{
			q=s;
			s=s->rchild;
		}
		(*p)->data=s->data; /*  s指向被刪結點的直接前驅(將被刪結點前驅的值取代被刪結點的值) */
		if(q!=*p)
			q->rchild=s->lchild; /*  重接q的右子樹 */ 
		else
			q->lchild=s->lchild; /*  重接q的左子樹 */
		free(s);
	}
	return TRUE;
}
else//後繼結點實現法
{
    q=*p,s=(*p)->rchild;//取右子樹
    while(s->lchild)//轉左,求直接後繼
    {
        q = s; s = s->lchild;
    }
    (*p)->data = s->data;
    if(q != *p) //z的直接後繼不為z的右孩子
        q->lchild = s->rchild;
    else
        q->rchild = s->rchild;
    free(s);    
}
        這裡,給出以下圖例來描述這個過程,如下圖所示,刪除結點47的過程:         這裡採用求前驅的方法,根據中序遍歷的性質我們可知,某結點的前驅即為該結點左子樹最右的結點。因此,由上圖可知,該子樹的前驅即為37。         根據迴圈,直到s指向左子樹最右結點為止。         複製結點資料到目的結點。         續接結點q的右子樹,如果s為待刪除結點的左子樹(即s沒有右孩子),那麼即需要重接q的左孩子(將s的左孩子作為q的左孩子,注意,這裡的每一個葉子結點都省略了其子結點指向NULL的事實)。         釋放結點。

小結

        二叉排序樹的查詢, 其比較次數等於給定值的結點在二叉排序樹中的層數。其時間效能取決於二叉樹的高度。然而問題在於,二叉排序樹的形狀是不確定的。它的平均時間複雜度為O(logn),而最壞的的時間複雜度為O(n)。這裡就引申出另一問題,即如何讓二叉排序樹平衡的問題。