二叉搜尋樹
概念
二叉搜尋樹又稱為二叉排序樹,因為這棵樹的中序遍歷是有序的。二叉搜尋樹總結起來有以下幾個性質:
- 若它的左子樹不為空,則左子樹上所有節點的值都小於根節點的值
- 若它的右子樹不為空,則右子樹上所有節點的值都大於於根節點的值
- 它的左右子樹都是二叉搜尋樹
- 這棵樹中沒有重複的元素
舉個例子:
二叉搜尋樹的實現
基本框架
template <class K,class V>
struct BST_Node
{
BST_Node<K,V>* left;
BST_Node<K,V>* right;
K key;
V value;
//建構函式
BST_Node(const K& key, const V& value):left(nullptr),right(nullptr),key(key),value(value)
{}
};
template <class K,class V>
class BST_Tree
{
typedef BST_Node<K,V> Node;
public:
private:
Node* root = nullptr;
};
要實現的介面
bool Insert(const K& key,const V& value);//二叉搜尋樹的插入
void InOrder();//列印功能,採用中序遍歷(遞迴)
Node* Find(const K& key);//二叉搜尋樹的查詢
bool Erase(const K& key);//二叉搜尋樹的刪除
二叉搜尋樹的插入
插入分為下面幾個步驟:
- 先判斷樹是否為空,為空就讓要插入的這個節點作為根節點,然後結束
- 確定要插入節點的位置
- 用一個cur記錄當前節點,parent記錄父節點
- 要插入節點的值如果比當前節點的值小,cur就往左走,如果比當前節點的值大,就往右子樹走,如果等於就返回false,表面這棵樹中有這個資料,不需要插入
//二叉搜尋樹的插入
bool Insert(const K& key,const V& value)
{
//沒有節點的時候就是根節點
if (root == nullptr)
{
root = new Node(key,value);
return true;
}
//用一個父節點記錄cur的上一個節點
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;
}
cur = new Node(key,value);
//判斷應該插在父節點的左邊還是右邊
if (cur->key < parent->key)
{
parent->left = cur;
}
else
{
parent->right = cur;
}
return true;
}
列印二叉搜尋樹(中序遍歷)
//中序遍歷(遞迴)
void InOrder()
{
_InOrder(root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == NULL)
{
return;
}
else
{
_InOrder(root->left);
cout << root->key << ":" << root->value << endl;
_InOrder(root->right);
}
}
二叉搜尋樹的查詢
查詢的步驟如下:(和插入的步驟有些類似)
- 如果查詢值key比當前節點的值小,就往左子樹走
- 如果查詢值key比當前節點的值大,就往右子樹走
- 如果查詢值key和當前節點的值相等,就返回當前節點的指標
//二叉搜尋樹的查詢
Node* Find(const K& key)
{
if (root == nullptr)
{
return nullptr;
}
Node* cur = root;//遍歷結點
while (cur)
{
//小於往左走
if (cur->key > key)
{
cur = cur->left;
}
else if (cur->key < key)
{
cur = cur->right;
}
else
{
return cur;
}
}
return nullptr;
}
二叉搜尋樹的刪除(難)
分四種情況:我們一個一個來討論
以下面這顆樹為例:
情景一:
情景二:
還要分析一種特殊的情況,就是此時2沒有父親節點,也就是自己為根時,看下面如何操作
情景三:
該節點如果為根節點,就讓自己的右孩子變成根節點
情景四:
總結: 一共有四種情況,但是情況1可以歸為情況3,因為它也是左為空,所以整體處理下來是三種情況
//二叉搜尋樹的刪除
bool Erase(const K& key)
{
//樹為空,刪除失敗
if (root == nullptr)
{
return false;
}
//parent始終是cur的父親節點
//cur就是要找的刪除的當前節點
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.左右子樹都不為空,取左子樹最大的節點或右子樹最小的節點和要刪除的節點交換,然後再刪除
//當前情況是情景三,刪除的節點它的左為空,右未知
if (cur->left == nullptr)
{
// 要刪除節點為根節點時,直接把右子樹的根節點賦值給——root
// 根節點的話會導致parent為nullptr
if (root == cur)
{
root = root->right;
}
else
{
//左為空,父親指向我的右
//判斷cur在父親的左還是右
if (parent->left == cur)
{
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
{
//找右子樹中最小的節點,當前cur就是要刪除的節點
Node* rightMinParent = cur;
Node* rightMin = cur->right;//去右子樹找最小的節點
while (rightMin->left)
{
rightMinParent = rightMin;
rightMin = rightMin->left;
}
//替代刪除
cur->key = rightMin->key;
//轉化成了情景三,左孩子為空
if (rightMinParent->left == rightMin)
rightMinParent->left = rightMin->right;
else
rightMinParent->right = rightMin->right;
delete rightMin;
rightMin = nullptr;
}
return true;
}
}
return false;
}
完整程式碼以及測試
#define _CRT_SECURE_NO_WARNINGS
#include<iostream> //引入標頭檔案
#include<string>//C++中的字串
using namespace std; //標準名稱空間
template <class K >
struct BST_Node
{
BST_Node<K>* left;
BST_Node<K>* right;
K key;
//建構函式
BST_Node(const K& key):left(nullptr),right(nullptr),key(key)
{}
};
template <class K>
class BST_Tree
{
typedef BST_Node<K> Node;
public:
//二叉搜尋樹的插入
bool Insert(const K& key)
{
//沒有節點的時候就是根節點
if (root == nullptr)
{
root = new Node(key);
return true;
}
//用一個父節點記錄cur的上一個節點
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;
}
cur = new Node(key);
//判斷應該插在父節點的左邊還是右邊
if (cur->key < parent->key)
{
parent->left = cur;
}
else
{
parent->right = cur;
}
return true;
}
//中序遍歷(遞迴)
void InOrder()
{
_InOrder(root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == NULL)
{
return;
}
else
{
_InOrder(root->left);
cout << root->key <<" ";
_InOrder(root->right);
}
}
//二叉搜尋樹的查詢
Node* Find(const K& key)
{
if (root == nullptr)
{
return nullptr;
}
Node* cur = root;//遍歷結點
while (cur)
{
//小於往左走
if (cur->key > key)
{
cur = cur->left;
}
else if (cur->key < key)
{
cur = cur->right;
}
else
{
return cur;
}
}
return nullptr;
}
//二叉搜尋樹的刪除
bool Erase(const K& key)
{
//樹為空,刪除失敗
if (root == nullptr)
{
return false;
}
//parent始終是cur的父親節點
//cur就是要找的刪除的當前節點
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.左右子樹都不為空,取左子樹最大的節點或右子樹最小的節點和要刪除的節點交換,然後再刪除
//當前情況是情景三,刪除的節點它的左為空,右未知
if (cur->left == nullptr)
{
// 要刪除節點為根節點時,直接把右子樹的根節點賦值給——root
// 根節點的話會導致parent為nullptr
if (root == cur)
{
root = root->right;
}
else
{
//左為空,父親指向我的右
//判斷cur在父親的左還是右
if (parent->left == cur)
{
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
{
//找右子樹中最小的節點,當前cur就是要刪除的節點
Node* rightMinParent = cur;
Node* rightMin = cur->right;//去右子樹找最小的節點
while (rightMin->left)
{
rightMinParent = rightMin;
rightMin = rightMin->left;
}
//替代刪除
cur->key = rightMin->key;
//轉化成了情景三,左孩子為空
if (rightMinParent->left == rightMin)
rightMinParent->left = rightMin->right;
else
rightMinParent->right = rightMin->right;
delete rightMin;
rightMin = nullptr;
}
return true;
}
}
return false;
}
private:
Node* root = nullptr;
};
void TestBSTree()
{
BST_Tree<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();
}
}
int main()
{
TestBSTree();
system("pause");
return EXIT_SUCCESS;
}
二叉搜尋樹的應用
二叉搜尋樹有兩種模型:
- K模型: K模型只有key值,節點只儲存key值。這裡主要應用就是查詢判斷某個元素在不在。
- KV模型: KV模型每個key值都對應著一個value,主要應用就是透過key找value。(我們平時查詢單詞就是透過中文找英文,或者透過英文找中文)
上面的測試程式碼是KV模型改成了K模型,接下來我們來看看KV模型的作用
#define _CRT_SECURE_NO_WARNINGS
#include<iostream> //引入標頭檔案
#include<string>//C++中的字串
using namespace std; //標準名稱空間
template <class K,class V>
struct BST_Node
{
BST_Node<K,V>* left;
BST_Node<K,V>* right;
K key;
V value;
//建構函式
BST_Node(const K& key, const V& value):left(nullptr),right(nullptr),key(key),value(value)
{}
};
template <class K,class V>
class BST_Tree
{
typedef BST_Node<K,V> Node;
public:
//二叉搜尋樹的插入
bool Insert(const K& key,const V& value)
{
//沒有節點的時候就是根節點
if (root == nullptr)
{
root = new Node(key,value);
return true;
}
//用一個父節點記錄cur的上一個節點
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;
}
cur = new Node(key,value);
//判斷應該插在父節點的左邊還是右邊
if (cur->key < parent->key)
{
parent->left = cur;
}
else
{
parent->right = cur;
}
return true;
}
//中序遍歷(遞迴)
void InOrder()
{
_InOrder(root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == NULL)
{
return;
}
else
{
_InOrder(root->left);
cout << root->key << ":" << root->value << endl;
_InOrder(root->right);
}
}
//二叉搜尋樹的查詢
Node* Find(const K& key)
{
if (root == nullptr)
{
return nullptr;
}
Node* cur = root;//遍歷結點
while (cur)
{
//小於往左走
if (cur->key > key)
{
cur = cur->left;
}
else if (cur->key < key)
{
cur = cur->right;
}
else
{
return cur;
}
}
return nullptr;
}
//二叉搜尋樹的刪除
bool Erase(const K& key)
{
//樹為空,刪除失敗
if (root == nullptr)
{
return false;
}
//parent始終是cur的父親節點
//cur就是要找的刪除的當前節點
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.左右子樹都不為空,取左子樹最大的節點或右子樹最小的節點和要刪除的節點交換,然後再刪除
//當前情況是情景三,刪除的節點它的左為空,右未知
if (cur->left == nullptr)
{
// 要刪除節點為根節點時,直接把右子樹的根節點賦值給——root
// 根節點的話會導致parent為nullptr
if (root == cur)
{
root = root->right;
}
else
{
//左為空,父親指向我的右
//判斷cur在父親的左還是右
if (parent->left == cur)
{
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
{
//找右子樹中最小的節點,當前cur就是要刪除的節點
Node* rightMinParent = cur;
Node* rightMin = cur->right;//去右子樹找最小的節點
while (rightMin->left)
{
rightMinParent = rightMin;
rightMin = rightMin->left;
}
//替代刪除
cur->key = rightMin->key;
//轉化成了情景三,左孩子為空
if (rightMinParent->left == rightMin)
rightMinParent->left = rightMin->right;
else
rightMinParent->right = rightMin->right;
delete rightMin;
rightMin = nullptr;
}
return true;
}
}
return false;
}
private:
Node* root = nullptr;
};
int main()
{
system("pause");
return EXIT_SUCCESS;
}
例項1
英漢字典
void TestBSTree_KV1()
{
// 建立一個簡易的字典
BST_Tree<string, string> dict;
dict.Insert("蘋果", "apple");
dict.Insert("香蕉", "banana");
dict.Insert("橘子", "orange");
dict.Insert("葡萄", "grape");
dict.Insert("apple", "蘋果");
dict.Insert("banana", "香蕉");
dict.Insert("orange", "橘子");
dict.Insert("grape", "葡萄");
string str;
while (cin >> str)
{
BST_Node<string, string>* ret = dict.Find(str);
if (ret)
{
cout << ret->value << endl;
}
else
{
cout << "本字典無此詞" << endl;
}
}
}
例項2
統計樹
void TestBSTree_KV2()
{
// 統計水果個數
BST_Tree<string, int> countTree;
string strArr[] = { "香蕉","水蜜桃","西瓜","蘋果","香蕉" ,"西瓜","香蕉" ,"蘋果","西瓜","蘋果","蘋果","香蕉" ,"水蜜桃" };
for (auto e : strArr)
{
BST_Node<string, int>* ret = countTree.Find(e);
if (ret == nullptr)
{
// 第一次插入
countTree.Insert(e, 1);
}
else
{
ret->value++;
}
}
countTree.InOrder();
}