最近在分析分析紅黑樹
時,感覺上來就挑最難的樹結構之一進行分析,難度太大,所以特意寫這篇二叉搜索樹
分析的博客作爲鋪墊。那麼爲啥挑二叉搜索樹
進行分析捏?其實紅黑樹
也是一種更爲複雜的二叉搜索樹
,建議閱讀一下我的另外一篇博客 數據結構之二叉樹、AVL樹、紅黑樹、Trie樹、B樹、B+樹、B*樹淺析 。爲了幫助大家理解紅黑樹
,先寫這篇博客分析二叉搜索樹
。下面將主要分析二叉搜索樹
的查找、插入、刪除三種操作,並且附上C++代碼實現。
二叉搜索樹詳解目錄
一、二叉搜索樹
簡述
二叉搜索樹
大致定義爲二叉樹的左子樹任意節點的值小於根節點的值,右子樹任意節點的值大於根節點的值,並且左子樹、右子樹同樣也符合二叉搜索樹
的定義(遞歸定義)。中序遍歷順序爲左根右
,所以二叉搜索樹
的典型特徵是中序遍歷序列有序。
二、二叉搜索
樹相關操作
爲了能讓大家更好的理解二叉搜索樹
,將提供C++的編碼實現。下面是TreeNode
結構體實現:
/**
* 這裏額外添加了parent指針,主要是爲了訪問父節點方便
*/
struct TreeNode {
int value;
// 三個指針分別指向父節點、左子樹根節點、右子樹根節點
TreeNode *parent;
TreeNode *left;
TreeNode *right;
TreeNode(int value) : value(value), parent(NULL), left(NULL), right(NULL) {}
TreeNode(int value, TreeNode *parent, TreeNode *left, TreeNode *right) {
this->value = value;
this->parent = parent;
this->left = left;
this->right =right;
}
};
查找
、插入
、刪除
節點三個操作的複雜度依次增加,如果覺得有點壓力,請按照順序依次閱讀,穩紮穩打。
1、二叉搜索樹
中節點的查找
二叉搜索樹
設計成左 < 根 < 右
(中序遍歷有序),一個很重要的動機就是快速查找。有過一點算法基礎的應該能想到有一種搜索策略非常相似,沒錯就是二分搜索
,每次將target
與搜索區間(遞增有序)的中間值mid
比較,如果target
< mid
則縮小搜索區間爲[left, mid - 1]
,如果target
> mid
則縮小搜索區間爲[mid + 1, right]
,否則target
== mid
。辣麼再來看看二叉搜索樹中查找的僞代碼。
root 指向二叉搜索樹的根,target爲需要搜索的值
while (root != NULL) {
if root->value == target
成功搜索到了 return
else if root->value > target
// 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
// 既然root->value > target,那麼只可能出現在左子樹,轉移root到左子樹
root = root->left
else
// 否則root->value < target
// 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
// 既然root->value < target,那麼只可能出現在右子樹,轉移root到右子樹
root = root->right
}
// root == NULL,二叉搜索樹中沒有target
C++代碼實現:
// 在二叉搜索樹中查找target,存在返回對應的指針,否則返NULL
TreeNode *searchNode(TreeNode *root, int target){
while (root != NULL) {
if (root->value == target) {
break;
}
else if (root->value > target) {
// 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
// 既然root->value > target,那麼只可能出現在左子樹,轉移root到左子樹
root = root->left;
}
else {
// 否則root->value < target
// 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
// 既然root->value < target,那麼只可能出現在右子樹,轉移root到右子樹
root = root->right;
}
}
return root;
}
2、二叉搜索樹
中節點的插入
二叉搜索樹
中查找充分利用左 < 根 < 右
特性,辣麼插入也能用上這個特性麼?答案是顯然的。
首先思考一下,我們插入節點後是不是還需要維持二叉樹
仍然滿足二叉搜索樹
特性,這是必須的,要不能你的二叉搜索樹
就變成一次性的了。辣麼我們就先要找到它真實應該插入的位置,保證中序遍歷爲遞增有序。下面是刪除的僞代碼。
root 指向二叉搜索樹的根,value爲需要插入的值
if root == NULL
// 二叉搜索樹爲空,插入的節點即是根節點
root = new TreeNode(value)
else
// 我們需要找到插入的位置
TreeNode *ptr = root;
while (true) {
if ptr->value == value
// 樹中已經存在這個value,不進行插入(這裏簡化邏輯)
break
else if ptr->value > value
// 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
// 既然ptr->value > value,value只能插入ptr左子樹
if ptr->left == NULL
// 如果ptr左子樹爲空,則插入的節點正好做左子樹的根
ptr->left = new TreeNode(value)
break
else
// 否則轉移到左子樹,繼續查找
ptr = ptr->left
else
// 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
// 既然ptr->value < value,value只能插入ptr右子樹
if ptr->right == NULL
// 如果ptr右子樹爲空,則插入的節點正好做右子樹的根
ptr->right = new TreeNode(value)
break
else
// 否則轉移到右子樹,繼續查找
ptr = ptr->right
}
return root
C++代碼實現:
// 在二叉搜索樹中插入value,如果二叉樹中已經存在則不進行插入(簡化處理邏輯)
TreeNode *searchNode(TreeNode *root, int value) {
if (root == NULL) {
// 二叉搜索樹爲空,插入的節點即是根節點
root = new TreeNode(value);
}
else {
// 我們需要找到插入的位置
TreeNode *ptr = root;
while (true) {
if (ptr->value == value) {
// 樹中已經存在這個value,不進行插入(這裏簡化邏輯)
break;
}
else if (ptr->value > value) {
// 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
// 既然ptr->value > value,value只能插入ptr左子樹
if (ptr->left == NULL) {
// 如果ptr左子樹爲空,則插入的節點正好做左子樹的根
ptr->left = new TreeNode(value);
break;
}
else {
// 否則轉移到左子樹,繼續查找
ptr = ptr->left;
}
}
else {
// 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
// 既然ptr->value < value,value只能插入ptr右子樹
if (ptr->right == NULL) {
// 如果ptr右子樹爲空,則插入的節點正好做右子樹的根
ptr->right = new TreeNode(value);
break;
}
else {
// 否則轉移到右子樹,繼續查找
ptr = ptr->right;
}
}
}
}
return root;
}
3、二叉搜索樹
中節點的刪除
在二叉搜索樹
刪除節點,首先我們應該在樹中查找到這個節點的位置吧,然後將其移除,並且移除後我們需要進行調整,使其任然滿足二叉搜索樹
。這個刪除操作可以分成好幾種情況,需要分別討論。
①、刪除葉節點
刪除葉節點
,只要將其移除即可,不需要進行任何調整操作。
②、刪除非葉節點
刪除非葉節點
可以細分爲兩種,第一種是刪除有右子樹的節點,刪除節點後需要將右子樹中序遍歷第一個節點填充到刪除節點A位置(爲什麼選右子樹中序遍歷第一個節點?因爲整棵樹的中序遍歷序列中,節點A的下一個節點就是其右子樹中序遍歷的第一個節點)。
右子樹中序遍歷第一個節點爲某個節點的左子節點,直接將最左的左子節點填補到已刪除節點的位置。
a、刪除節點有右子樹
右子樹中序遍歷序列中的第一個節點爲某個沒有左子節點的節點B。將節點B填入已刪除節點的位置,並且將節點B的右子樹置於節點B的位置。
b、刪除節點沒有右子樹
第二種是刪除沒有右子樹的節點A,此時尋找整顆二叉樹中序遍歷中節點A的下一個節點,稍微複雜一點,需要利用parent指針。找到遠祖父節點B,並且使得節點A在遠祖父節點B的左子樹中
!遠祖父節點B.value替換到節點A.value後,它自己也需要它的下一個節點來填充。
其實根本不需要進行刪除操作,只要尋找到這個節點中序遍歷序列的下一個節點,然後直接替換即可。
C++代碼實現:
// 在二叉搜索樹中插入value,如果二叉樹中已經存在則不進行插入(簡化處理邏輯)
TreeNode *deleteNode(TreeNode *root, TreeNode *targetPtr) {
if (targetPtr == NULL || root == NULL) {
return root;
}
if (root == targetPtr && root->right == NULL) {
// 一、刪除的是根節點,並且根節點沒有右子樹
// 處理:切斷targetPtr與左子樹的關聯,返回左子樹,釋放targetPtr
root = root->left;
if (root != NULL) {
root->parent = NULL;
}
targetPtr->left = NULL;
delete targetPtr;
}
else if (targetPtr->left == targetPtr->right) {
// 二、刪除的葉節點
// targetPtr->left == targetPtr->right,只能是同時爲NULL
// 操作:切斷parent與targetPtr的關聯,返回root,釋放targetPtr
if (targetPtr->parent->left == targetPtr) {
targetPtr->parent->left = NULL;
} else {
targetPtr->parent->right = NULL;
}
// 切斷targetPtr 與targetPtr->parent的關聯
targetPtr->parent = NULL;
delete targetPtr;
} else if (targetPtr->right == NULL) {
// 三、刪除節點右子樹爲空 需要找到遠祖父節點
TreeNode *pParent = targetPtr;
// 祖父節點B,並且使得節點targetPtr在遠祖父節點B的左子樹
while (pParent->parent != NULL && pParent->parent->right == pParent) {
pParent = pParent->parent;
}
if (pParent->parent == NULL) {
// 如果targetPtr不存在一個遠祖父節點B,使得leftPtr在遠祖父B的左子樹
// 操作:只能刪除這個節點,並且把左子樹放到當前節點的位置
targetPtr->parent->right = targetPtr->left;
targetPtr->left->parent = targetPtr->parent;
// 切斷targetPtr與parent、left的關係
targetPtr->parent = NULL;
targetPtr->left = NULL;
delete targetPtr;
}
else {
// 否則pParent->parent->value替換targetPtr->value,再刪除pParent->parent(遞歸)
targetPtr->value = pParent->parent->value;
root = deleteNode(root, pParent->parent);
}
}
else {
// 四、刪除節點存在右子樹,直接在右子樹尋找中序遍歷的第一個節點
TreeNode * leftPtr = targetPtr->right;
// 一直往left尋找
while (leftPtr->left != NULL) {
leftPtr = leftPtr->left;
}
// 將leftPtr->value替換到targetPtr->value
targetPtr->value = leftPtr->value;
// targetPtr->right就是leftPtr
if (targetPtr->right == leftPtr) {
// 將targetPtr->right指向leftPtr右子樹
targetPtr->right = leftPtr->right;
// 如果targetPtr->right != NULL,還需要設置parent
if (leftPtr->right != NULL) {
leftPtr->right->parent = targetPtr;
}
}
else {
// 否則leftPtr->parent與leftPtr切斷關係
leftPtr->parent->left = NULL;
}
// 將leftPtr於其parent切斷關係並釋放
leftPtr->parent = NULL;
delete leftPtr;
}
return root;
}
TreeNode *deleteNodeByValue(TreeNode *root, int value) {
// 首先查找value所在的位置
TreeNode *targetPtr = searchNode(root, value);
if (targetPtr == NULL) {
// value都沒找到還刪除啥...
return root;
}
return deleteNode(root, targetPtr);
}
三、思考與總結
可以看出二叉搜索樹
的查找、插入還是比較簡單的,刪除稍微複雜一點。不過二叉搜索樹
可能存在退化成鏈表的缺陷,比如給你一個本來遞增有序的序列讓你插入到一顆空二叉搜索樹中,這時就退化成鏈表了。
因此我們需要將二叉搜索樹
增加平衡
的特性,即AVL樹
或紅黑樹
。後面有時間會更新一篇紅黑樹
的博客,敬請期待~