C++數(shù)據(jù)結(jié)構(gòu)二叉搜索樹的實現(xiàn)應(yīng)用與分析
??博客代碼已上傳至gitee:https://gitee.com/byte-binxin/cpp-class-code
??概念
二叉搜索樹又稱為二叉排序書,因為這棵樹的中序遍歷是有序的。二叉搜索樹總結(jié)起來有以下幾個性質(zhì):
- 若它的左子樹不為空,則左子樹上所有節(jié)點的值都小于根節(jié)點的值
- 若它的右子樹不為空,則右子樹上所有節(jié)點的值都大于于根節(jié)點的值
- 它的左右子樹都是二叉搜索樹
- 這棵樹中沒有重復(fù)的元素

??二叉搜索樹的實現(xiàn)
??基本框架
由一個節(jié)點的成員構(gòu)成,先構(gòu)建節(jié)點的類型,和我們之前數(shù)據(jù)結(jié)構(gòu)中的二叉樹的節(jié)點定義是一樣的。二叉搜索樹的根節(jié)點先默認(rèn)給空。
template <class K, class V>
struct BSTNode
{
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
K _key;
V _value;
BSTNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
,_value(value)
{}
};
template <class K, class V>
class BSTree //Binary Search Tree
{
typedef BSTNode<K, V> Node;
private:
Node* _root = nullptr;
};
??二叉搜索樹的插入
插入分為下面幾個步驟:
- 先判斷樹是否為空,為空就讓要插入的這個節(jié)點作為根節(jié)點,然后結(jié)束
- 部署就確定要插入節(jié)點的位置
- 用一個cur記錄當(dāng)前節(jié)點,parent記錄父節(jié)點
- 要插入節(jié)點的值如果比當(dāng)前節(jié)點的值小,cur就往左走,如果比當(dāng)前節(jié)點的值大,就往右子樹走,如果等于就返回false,表面這棵樹中有這個數(shù)據(jù),不需要插入。
下面是一個簡單的動圖演示

注意: 這里不用擔(dān)心新插入節(jié)點會在樹中間插入,它一定是在最下面插入的,它會走到最下面,然后在樹的底部插入。
代碼實現(xiàn)如下:
bool Insert(const K& key, const V& value)
{
// 沒有節(jié)點時第一個節(jié)點就是根節(jié)點
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
// 用一個父親節(jié)點記錄cur的上一個節(jié)點
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
parent = cur;
// 小于往左邊走
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return false;// 已有的節(jié)點不插入,此次插入失敗
}
cur = new Node(key, value);
// 判斷應(yīng)該插在父節(jié)點的左邊還是右邊
if (cur->_key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
為了更好地觀察這棵樹插入后是否有效,我們可以實現(xiàn)一個中序遍歷,將其打印出來。 中序遍歷代碼如下:
void InOrder()
{
// 利用子函數(shù)遍歷
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
測試代碼如下:
void TestBSTree()
{
BSTree<int> bt;
int arr[] = { 5,3,4,1,7,8,2,6,0,9 };
//int arr[] = { 1,2,3,4 };
//int arr[] = { 4,3,2,1};
for (auto e : arr)
{
bt.Insert(e);
}
bt.InOrder();
}
代碼運行結(jié)果如下:

??二叉搜索樹的查找
查找的步驟如下:(和插入的步驟有些類似)
- 如果查找值key比當(dāng)前節(jié)點的值小,就往左子樹走
- 如果查找值key比當(dāng)前節(jié)點的值大,就往右子樹走
- 如果查找值key和當(dāng)前節(jié)點的值相等,就返回當(dāng)前節(jié)點的指針
代碼實現(xiàn)如下:
Node* Find(const K& key)
{
if (_root == nullptr)
return nullptr;
Node* cur = _root;
while (cur)
{
// 小于往左邊走
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
??二叉搜索樹的刪除(重點)
二叉搜索樹的刪除相對來說會復(fù)雜一些,下面我要給大家分析一下。 有四種情況 先看下面這棵樹,分別對以下四個節(jié)點進行刪除會發(fā)生什么(如何處理)?

- 刪除節(jié)點1時,它的左右都為空,可以直接刪除
- 刪除節(jié)點2時,它的左不為空右為空,刪除方法如下:

還要分析一種特殊的情況,就是此時2沒有父親節(jié)點,也就是自己為根時,看下面如何操作

- 刪除節(jié)點7時,它的左為為右不為空,刪除方法如下:

和情況2一樣,該節(jié)點如果為根節(jié)點,就讓自己的右孩子變成根節(jié)點。
- 左右都不為空(替代法)
這種情況我們采用替代法來解決,替代法就是找一個節(jié)點和現(xiàn)在這個節(jié)點交換,然后轉(zhuǎn)移為上面的情況,具體如下: 我們可以選擇用左子樹的最右節(jié)點(左子樹最大的節(jié)點)或右子樹的最左節(jié)點(右子樹的最小節(jié)點)和當(dāng)前節(jié)點互換,然后刪除互換后的節(jié)點,這里我們統(tǒng)一采用用右子樹的最右節(jié)點來進行替換。

然后這里可以轉(zhuǎn)化為情況3來對節(jié)點進行刪除,因為所有的最左孩子一定是左為空,右是不確定的。
總結(jié): 一共有四種情況,但是情況1可以歸為情況3,因為它也是左為空,所以整體處理下來是三種情況。
代碼實現(xiàn)如下:
bool Erase(const K& key)
{
// 如果樹為空,刪除失敗
if (_root == nullptr)
return false;
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// 小于往左邊走
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
// 找到了,開始刪除
// 1.左右子樹都為空 直接刪除 可以歸類為左為空
// 2.左右子樹只有一邊為空 左為空,父親指向我的右,右為空,父親指向我的左
// 3.左右子樹都不為空 取左子樹最大的節(jié)點或右子樹最小的節(jié)點和要刪除的節(jié)點交換,然后再刪除
if (cur->_left == nullptr)
{
// 要刪除節(jié)點為根節(jié)點時,直接把右子樹的根節(jié)點賦值給——root
// 根節(jié)點的話會導(dǎo)致parent為nullptr
if (_root == cur)
{
_root = _root->_right;
}
else
{
// 左為空,父親指向我的右
// 判斷cur在父親的左還是右
if (parent->_left == cur) // cur->_key < parent->_key
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = _root->_left;
}
else
{
// 右為空,父親指向我的左
// 判斷cur在父親的左還是右
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
cur = nullptr;
}
else
{
// 找右子樹中最小的節(jié)點
Node* rightMinParent = cur;
Node* rightMin = cur->_right;// 去右子樹找
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
//swap(cur->_key, rightMin->_key);
// 替代刪除
cur->_key = rightMin->_key;
// 轉(zhuǎn)換成了第一種情況 左為空
if (rightMinParent->_left == rightMin)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
rightMin = nullptr;
}
return true;
}
}
return false;
}
測試代碼如下:(要測試每種情況,還有測試刪空的情況)
void TestBSTree()
{
BSTree<int> bt;
int arr[] = { 5,3,4,1,7,8,2,6,0,9 };
for (auto e : arr)
{
cout << "插入 " << e << " 后:";
bt.Insert(e);
bt.InOrder();
}
cout << "------------------------------" << endl;
for (auto e : arr)
{
cout << "刪除 " << e << " 后:";
bt.Erase(e);
bt.InOrder();
}
}
代碼運行結(jié)果如下:

??二叉搜索樹的應(yīng)用
二叉搜索樹有兩種模型:
- K模型: K模型只有key值,節(jié)點只存儲key值。這里主要應(yīng)用就是查找判斷某個元素在不在。
- KV模型: KV模型每個key值都對應(yīng)著一個value,主要應(yīng)用就是通過key找value。(我們平時查找單詞就是通過中文找英文,或者通過英文找中文)
下面我把上面的K模型的代碼簡單改造一下,實現(xiàn)KV模型:(這里沒有使用傳鍵值對的方法,之后的博客我會給大家介紹,這里使用傳兩個值的方式)
template <class K, class V>
struct BSTNode
{
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
K _key;
V _value;
BSTNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
,_value(value)
{}
};
template <class K, class V>
class BSTree //Binary Search Tree
{
typedef BSTNode<K, V> Node;
public:
~BSTree()
{
Node* cur = _root;
while (cur)
{
Erase(cur->_key);
cur = _root;
}
}
Node* Find(const K& key)
{
if (_root == nullptr)
return nullptr;
Node* cur = _root;
while (cur)
{
// 小于往左邊走
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
bool Insert(const K& key, const V& value)
{
// 沒有節(jié)點時第一個節(jié)點就是根節(jié)點
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
// 用一個父親節(jié)點記錄cur的上一個節(jié)點
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
parent = cur;
// 小于往左邊走
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return false;// 已有的節(jié)點不插入,此次插入失敗
}
cur = new Node(key, value);
// 判斷應(yīng)該插在父節(jié)點的左邊還是右邊
if (cur->_key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
bool Erase(const K& key)
{
// 如果樹為空,刪除失敗
if (_root == nullptr)
return false;
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// 小于往左邊走
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
// 找到了,開始刪除
// 1.左右子樹都為空 直接刪除 可以歸類為左為空
// 2.左右子樹只有一邊為空 左為空,父親指向我的右,右為空,父親指向我的左
// 3.左右子樹都不為空 取左子樹最大的節(jié)點或右子樹最小的節(jié)點和要刪除的節(jié)點交換,然后再刪除
if (cur->_left == nullptr)
{
// 要刪除節(jié)點為根節(jié)點時,直接把右子樹的根節(jié)點賦值給——root
// 根節(jié)點的話會導(dǎo)致parent為nullptr
if (_root == cur)
{
_root = _root->_right;
}
else
{
// 左為空,父親指向我的右
// 判斷cur在父親的左還是右
if (parent->_left == cur) // cur->_key < parent->_key
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = _root->_left;
}
else
{
// 右為空,父親指向我的左
// 判斷cur在父親的左還是右
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
cur = nullptr;
}
else
{
// 找右子樹中最小的節(jié)點
Node* rightMinParent = cur;
Node* rightMin = cur->_right;// 去右子樹找
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
//swap(cur->_key, rightMin->_key);
// 替代刪除
cur->_key = rightMin->_key;
// 轉(zhuǎn)換成了第一種情況 左為空
if (rightMinParent->_left == rightMin)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
rightMin = nullptr;
}
return true;
}
}
return false;
}
void InOrder()
{
// 利用子函數(shù)遍歷
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
void TestBSTree_KV1()
{
// 創(chuàng)建一個簡易的字典
BSTree<string, string> dict;
dict.Insert("蘋果", "apple");
dict.Insert("排序", "sort");
dict.Insert("培養(yǎng)", "cultivate");
dict.Insert("通過", "pass");
dict.Insert("apple", "蘋果");
dict.Insert("sort", "排序");
dict.Insert("cultivate", "培養(yǎng)");
dict.Insert("pass", "通過");
string str;
while (cin >> str)
{
BSTNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << ret->_value << endl;
}
else
{
cout << "本字典無此詞" << endl;
}
}
下面測試幾個應(yīng)用: 實例1 英漢字典
void TestBSTree_KV1()
{
// 創(chuàng)建一個簡易的字典
BSTree<string, string> dict;
dict.Insert("蘋果", "apple");
dict.Insert("排序", "sort");
dict.Insert("培養(yǎng)", "cultivate");
dict.Insert("通過", "pass");
dict.Insert("apple", "蘋果");
dict.Insert("sort", "排序");
dict.Insert("cultivate", "培養(yǎng)");
dict.Insert("pass", "通過");
string str;
while (cin >> str)
{
BSTNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << ret->_value << endl;
}
else
{
cout << "本字典無此詞" << endl;
}
}
}
代碼運行結(jié)果演示:

實例2: 統(tǒng)計樹
void TestBSTree_KV2()
{
// 統(tǒng)計水果個數(shù)
BSTree<string, int> countTree;
string strArr[] = { "香蕉","水蜜桃","西瓜","蘋果","香蕉" ,"西瓜","香蕉" ,"蘋果","西瓜","蘋果","蘋果","香蕉" ,"水蜜桃" };
for (auto e : strArr)
{
BSTNode<string, int>* ret = countTree.Find(e);
if (ret == nullptr)
{
// 第一次插入
countTree.Insert(e, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
代碼運行結(jié)果如下:

??二叉樹性能分析
一般情況下,二叉搜索樹的插入和刪除的效率都是O(logN),極端情況會導(dǎo)致效率變成O(N)。
理想狀態(tài): 完全二叉樹:O(logN)

極端情況: 一條鏈:O(1)

后面我要和大家分析的AVL樹會利用旋轉(zhuǎn),就可解決掉這種極端情況。
??總結(jié)
上面這些是二叉搜索樹的大致內(nèi)容,其中刪除大家可以好好理解一下,它后面還有兩棵樹我還沒有介紹,就是AVL樹和紅黑樹,在后面兩篇博客我都會介紹。今天就先到這了,喜歡的話,歡迎點贊支持~

到此這篇關(guān)于C++數(shù)據(jù)結(jié)構(gòu)二叉搜索樹的實現(xiàn)應(yīng)用與分析的文章就介紹到這了,更多相關(guān)C++ 二叉搜索樹內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實現(xiàn)當(dāng)前時間動態(tài)顯示的方法
這篇文章主要介紹了C++實現(xiàn)當(dāng)前時間動態(tài)顯示的方法,涉及C++時間操作及Sleep方法的使用,具有一定參考借鑒價值,需要的朋友可以參考下2015-07-07
C/C++?Qt?數(shù)據(jù)庫與TableView實現(xiàn)多組件聯(lián)動
Qt?數(shù)據(jù)庫組件與TableView組件實現(xiàn)聯(lián)動效果,本文通過案例給大家講解的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-12-12

