資料結構之二叉排序樹
上一節我們介紹了二分(折半)查詢,也瞭解了它的優缺點。
二分查詢的特點:二分查詢能夠提高有序表中資料元素的查詢速度;二分查詢的時間複雜度為O(log2n);二分查詢是一種靜態查詢
二分查詢的不足:當查詢表經常變化時,二分查詢的整體效能急劇下降。二分查詢的硬傷:二分查詢基於有序表。
當需要插入或者刪除資料元素時,為了能夠繼續進行二分查詢,需要大規模挪動有序表中的資料元素,使得插入或者刪除後的線性表保持有序。二分查詢的過程是一棵二叉樹!如下圖:
這顆二叉樹的特性如下:
1.任意一個結點的值都大於其左子樹的所有結點值;
2.任意一個結點的值都小於其右子樹的所有結點值。
如何改進二分查詢使其適應動態查詢?這裡就有了一個新的想法,直接組織一棵具有二分查詢特性的二叉樹。二分查詢過程即變換為對樹結點的查詢過程;由二分查詢的特性可知樹結點查詢的時間複雜度為O(log2
二叉排序樹的定義:二叉排序樹是一棵空樹,或者,若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;它的左右子樹也分別是二叉排序樹。
二叉排序樹是特殊的二叉樹,因此具有與二叉樹相同的操作。只不過是插入與刪除時與普通的二叉樹有所不同而已,下面介紹一下二叉排序樹的插入與刪除操作:
二叉排序樹的插入和刪除操作
1.插入:其插入操作總是在葉結點處進行;
2.刪除;
2.1.
2.2.非葉結點:查詢合適的替代者後刪除。
二叉排序樹的所有操作都必須保證其二叉排序性不變。
那麼,如何為刪除操作查詢合適的替代者:
1.有一個孩子的結點:用孩子結點代替原結點;
2. 有兩個孩子的結點:用中序遍歷下的直接前驅替換原結點。
下面介紹一下二叉排序樹的插入與刪除實現程式碼:
1.插入
根據上面的描述,我們知道二叉排序樹的插入需要遞迴來實現,所以首先看一下二叉排序樹插入的遞迴函式
// 二叉排序樹插入遞迴函式 static int recursive_insert(BSTreeNode* root, BSTreeNode* node, BSTree_Compare* compare) { int ret = 1; int r = compare(node->key, root->key); // 二叉排序樹中含有相同值,非法 if( r == 0 ) { ret = 0; } // 插入元素大於根結點 // 在左子樹位置插入 else if( r < 0 ) { // 左子樹不為空、呼叫二叉排序樹插入遞迴函式,直至插入至左子樹 if( root->left != NULL ) { ret = recursive_insert(root->left, node, compare); } // 左子樹為空,直接插入 else { root->left = node; } } // 插入元素小於根結點 // 在右子樹位置插入 else if( r > 0 ) { // 右子樹不為空、呼叫二叉排序樹插入遞迴函式,直至插入至右子樹 if( root->right != NULL ) { ret = recursive_insert(root->right, node, compare); } // 右子樹為空,直接插入 else { root->right = node; } } }
插入程式碼如下:
// 根據引數插入結點至二叉排序樹
int BSTree_Insert(BSTree* tree, BSTreeNode* node, BSTree_Compare* compare)
{
// 定義二叉排序樹結構體變數並強制轉換引數
TBSTree* btree = (TBSTree*)tree;
// 入口引數合法性檢查
int ret = (btree != NULL) && (node != NULL) && (compare != NULL);
// 入口引數合法性ok
if( ret )
{
node->left = NULL;
node->right = NULL;
// 插入位置為根結點
if( btree->root == NULL )
{
btree->root = node;
}
// 呼叫二叉排序樹遞迴函式,尋找插入位置
else
{
ret = recursive_insert(btree->root, node, compare);
}
// 二叉排序樹結點個數加1
if( ret )
{
btree->count++;
}
}
return ret;
}
插入的結點一定是一個新新增的葉子結點,並且是查詢不成功時查詢路徑上訪問的最後一個結點的左孩子或者右孩子結點。同插入一樣,二叉排序樹的刪除也要通過遞迴來實現,遞迴函式如下:
static BSTreeNode* delete_node(BSTreeNode** pRoot)
{
BSTreeNode* ret = *pRoot;
// 有一個孩子的結點,用孩子結點代替原結點
// 右孩子為空,有一個左孩子的結點,用左孩子結點代替原結點
if( (*pRoot)->right == NULL )
{
*pRoot = (*pRoot)->left;
}
// 左孩子為空,有一個右孩子的結點,用右孩子結點代替原結點
else if( (*pRoot)->left == NULL )
{
*pRoot = (*pRoot)->right;
}
// 有兩個孩子的結點
else
{
BSTreeNode* g = *pRoot; // 儲存要刪除結點的地址
BSTreeNode* c = (*pRoot)->left; // 儲存要刪除結點的左孩子結點地址
// 迴圈移動要刪除結點的左孩子的右孩子,直至到葉結點為止,轉左,然後向右到盡頭
// 相當於用中序遍歷下的直接前驅
while( c->right != NULL )
{
g = c;
c = c->right; // 一直查詢右孩子
}
// 要刪除結點的左孩子有右孩子,用最後右葉子結點
if( g != *pRoot )
{
g->right = c->left;
}
// 要刪除結點的左孩子沒有右孩子,直接用左孩子代替要刪除結點
else
{
g->left = c->left;
}
// 將要刪除結點的雙親結點的左右孩子結點指向該結點的左右孩子結點
c->left = (*pRoot)->left;
c->right = (*pRoot)->right;
*pRoot = c;
}
return ret;
}
// 刪除二叉排序樹結點遞迴函式
static BSTreeNode* recursive_delete(BSTreeNode** pRoot, BSKey* key, BSTree_Compare* compare)
{
BSTreeNode* ret = NULL;
// 引數合法,樹存在
if( (pRoot != NULL) && (*pRoot != NULL) )
{
// 返回關鍵字與根結點比較結果
int r = compare(key, (*pRoot)->key);
// 找到關鍵字,呼叫刪除結點函式刪除結點
if( r == 0 )
{
ret = delete_node(pRoot);
}
// 獲取元素大於根結點
// 呼叫刪除二叉排序樹結點遞迴函式,從左子樹中開始
else if( r < 0 )
{
ret = recursive_delete(&((*pRoot)->left), key, compare);
}
// 獲取元素小於根結點
// 呼叫刪除二叉排序樹結點遞迴函式,從右子樹中開始
else if( r > 0 )
{
ret = recursive_delete(&((*pRoot)->right), key, compare);
}
}
return ret;
}
刪除結點函式如下:
// 刪除二叉排序樹指定結點
BSTreeNode* BSTree_Delete(BSTree* tree, BSKey* key, BSTree_Compare* compare)
{
// 定義二叉排序樹結構體變數並強制轉換引數
TBSTree* btree = (TBSTree*)tree;
BSTreeNode* ret = NULL;
// 入口引數合法性檢查ok
if( (btree != NULL) && (key != NULL) && (compare != NULL) )
{
// 呼叫刪除二叉排序樹結點遞迴函式
ret = recursive_delete(&btree->root, key, compare);
// 二叉排序樹結點個數減1
if( ret != NULL )
{
btree->count--;
}
}
return ret;
}
由於二叉排序樹的其他操作和普通二叉樹相同,這裡不再贅述,詳請參考二叉樹的建立
二叉排序樹整體程式碼:二叉排序樹的C程式碼實現