文章目錄
- 前言
- 1、二叉搜尋樹
-
- 1-1、 二叉搜尋樹概念
- 2、二叉搜尋樹操作
-
- 2-1、樹和節點的基本架構
- 2-2、二叉搜尋樹的查找
- 2-3、中序周遊
- 2-4、二叉搜尋樹的插入
- 2-5、二叉搜尋樹的删除
- 3、二叉搜尋樹的模拟實作
-
- 3-1、循環版本
- 3-2、遞歸版本
- 4、二叉搜尋樹的應用
-
- 4-1、K模型
- 4-2、KV模型
- 4-3、KV模型的代碼樣例
- 5、二叉搜尋樹的性能分析
- 6、總結
前言
二叉樹在前面C資料結構階段已經講過,本節取名二叉樹進階是因為:
- map和set特性需要先鋪墊二叉搜尋樹,而二叉搜尋樹也是一種樹形結構
- 二叉搜尋樹的特性了解,有助于更好的了解map和set的特性
- 二叉樹中部分面試題稍微有點難度,在前面講解大家不容易接受,且時間長容易忘
- 有些OJ題使用C語言方式實作比較麻煩,比如有些地方要傳回動态開辟的二維數組,非常麻煩。
是以本節借二叉樹搜尋樹,對二叉樹部分進行收尾總結。
1、二叉搜尋樹
1-1、 二叉搜尋樹概念
二叉搜尋樹又稱二叉排序樹,它或者是一棵空樹,或者是具有以下性質的二叉樹:
若它的左子樹不為空,則左子樹上所有節點的值都小于根節點的值
若它的右子樹不為空,則右子樹上所有節點的值都大于根節點的值
它的左右子樹也分别為二叉搜尋樹

2、二叉搜尋樹操作
2-1、樹和節點的基本架構
template<class k>
class BSTreeNode //結構體——包含節點,左指針和右指針
{
public:
BSTreeNode<k>* _left;//節點的左指針
BSTreeNode<k>* _right;//節點的右指針
k _key;//節點的值
BSTreeNode(const k& key)//構造函數,不寫的話new生成不了節點
:_left(nullptr)
, _right(nullptr)
,_key(key)
{}
};
template<class k>
class BSTree
{
typedef BSTreeNode<k> Node;//這裡重定義節點,友善後面的操作
public:
Node* _root = nullptr;//根節點
2-2、二叉搜尋樹的查找
a、從根開始比較,查找,比根大則往右邊走查找,比根小則往左邊走查找。
b、最多查找高度次,走到到空,還沒找到,這個值不存在。
bool Find(const k& key)//查找key值
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)//如果查找的值大,向右繼續查找
{
cur = cur->_right;
}
else if (cur->_key > key)//如果查找的值小,向左繼續查找
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
2-3、中序周遊
對于二叉搜尋樹而言,每一個節點的左子樹值都比該節點的值小,右子樹的值都比該節點的值要大。是以,中序周遊二叉搜尋樹是可以得到一個升序序列的!
void _InOrder(Node * root)//中序周遊
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
2-4、二叉搜尋樹的插入
插入的具體過程如下:
a. 樹為空,則直接新增節點,指派給root指針
b. 樹不空,按二叉搜尋樹性質查找插入位置,插入新節點
bool Insert(const k& key)//插入
{
if (_root == nullptr)//空樹直接new節點就行
{
_root = new Node(key);
return true;
}
Node* cur = _root;//cur從根開始找
Node* parent = nullptr;//記錄父節點
while (cur)
{
if (cur->_key < key)//我比你大,就向右找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)//我比你小,就向左找
{
parent = cur;
cur = cur->_left;
}
else不存在一樣的,二叉搜尋樹不允許有重複的值
{
return false;
}
}
cur = new Node(key);//到這裡就找到了我們要插入節點的為止,我們new(key)
if (parent->_key < key)//如果父親的_key值小于我們,就連結在父節點的右邊
{
parent->_right = cur;
}
else//如果父親的_key值大于我們,就連結在父節點的左邊
{
parent->_left = cur;
}
return true;
}
2-5、二叉搜尋樹的删除
首先查找元素是否在二叉搜尋樹中,如果不存在,則傳回, 否則要删除的結點可能分下面四種情況:
a. 要删除的結點無孩子結點
b. 要删除的結點隻有左孩子結點
c. 要删除的結點隻有右孩子結點
d. 要删除的結點有左、右孩子結點
看起來有待删除節點有4中情況,實際情況a可以與情況b或者c合并起來:
b. 右子樹為空
c. 左子樹為空
d. 左右子樹都不為空
是以真正的删除過程如下:
情況b:删除該結點且使被删除節點的雙親結點指向被删除節點的左孩子結點–直接删除
情況c:删除該結點且使被删除節點的雙親結點指向被删除結點的右孩子結點–直接删除
情況d:在它的右子樹中尋找中序下的第一個結點(關鍵碼最小:右子樹的最左值,也就是右子樹最小節點),用它的值填補到被删除節點 中,再來處理該結點的删除問題–替換法删除
情況b:
情況c:
情況d:
我們來看看代碼:
bool Erase(const k& key)//删除
{
Node* cur = _root;//cur向下面走,找删除節點的key值
Node* parent = nullptr;//parent就是删除節點的父節點——也就是cur的父節點
while (cur)
{
if (cur->_key < key)//老規矩,比你大向右找
{
parent = cur;//這個時候父節點要更新
cur = cur->_right;
}
else if (cur->_key > key)//比你小向左找
{
parent = cur;//同理
cur = cur->_left;
}
else到這裡就找到了
{
//三種情況:
// 1、左為空
// 2、右為空
// 3、左右都不為空,替換删除
if (cur->_left == nullptr)// 1、左為空
{
//if(parent == nullptr)
if (cur == _root)//如果是一顆單枝樹,删除根節點,parent就為空了,那麼parent->_left等操作違法
{
//這裡的cur和_root都是根節點了,我們讓_root等于_root的右節點,這樣下面删除cur,
//也就是删除原來的根節點了,而_root現在變成了原來樹的右子樹的第一個節點
_root = cur->_right;
}
else
{
if (parent->_left == cur)//如果父節點的左邊是cur,删除cur之後,cur的右子樹連結父親左邊
{
parent->_left = cur->_right;
}
else//如果父節點的右邊是cur,删除cur之後,cur的右子樹連結父親右邊
{
parent->_right = cur->_right;
}
}
delete cur;//這個時候删除就沒問題了
}
else if (cur->_right == nullptr)// 2、右為空
{
//if(parent == nullptr)
if (cur == _root)//如果是一顆單枝樹,删除根節點,parent就為空了,那麼parent->_left等操作違法
{
//這裡的cur和_root都是根節點了,我們讓_root等于_root的左節點,這樣下面删除cur,
//也就是删除原來的根節點了,而_root現在變成了原來樹的左子樹的第一個節點
_root = cur->_left;
}
else
{
if (parent->_left == cur)//如果父節點的左邊是cur,删除cur之後,cur的右左子樹連結父親左邊
{
parent->_left = cur->_left;
}
else//如果父節點的右邊是cur,删除cur之後,cur的左子樹連結父親右邊
{
parent->_right = cur->_left;
}
}
delete cur;
}
else// 3、左右都不為空,替換删除
{
//這裡parent不能初始化為nullptr,如果删除的cur是根,那麼parent就不會進入while循環進行更新
//那麼parent就一直為空,下面parent->_left等操作就非法了!!!
Node* parent = cur; //parent就是替換完之後,删除節點的父節點
Node* min = cur->_right;//min是右子樹的最小值
while (min->_left)//找右子樹最小的數,(也可以找左子樹最大的數,但是這樣太麻煩了)
//while (min && min->_left)//這裡不用這麼寫,因為前提條件就是cur删除節點左右都不為空
{
parent = min;
min = min->_left;
}
cur->_key = min->_key;//把右子樹最小值給給cur
if (parent->_left == min)//min是最左值,是以沒有左子樹,隻有右子樹需要托孤
{
parent->_left = min->_right;
}
else
{
parent->_right = min->_right;
}
delete min;
}
return true;
}
}
return false;
}
3、二叉搜尋樹的模拟實作
這裡有兩個版本:循環和遞歸
我們隻用掌握一個版本就可以了!
3-1、循環版本
BSTree.h:循環版本
#pragma once
///循環版本
template<class k>
class BSTreeNode //結構體——包含節點,左指針和右指針
{
public:
BSTreeNode<k>* _left;
BSTreeNode<k>* _right;
k _key;
BSTreeNode(const k& key)//構造函數,不寫的話new生成不了節點
:_left(nullptr)
, _right(nullptr)
,_key(key)
{}
};
template<class k>
class BSTree
{
typedef BSTreeNode<k> Node;
public:
bool Insert(const k& key)//插入
{
if (_root == nullptr)//空樹直接new節點就行
{
_root = new Node(key);
return true;
}
Node* cur = _root;//cur從根開始找
Node* parent = nullptr;//記錄父節點
while (cur)
{
if (cur->_key < key)//我比你大,就向右找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)//我比你小,就向左找
{
parent = cur;
cur = cur->_left;
}
else不存在一樣的,二叉搜尋樹不允許有重複的值
{
return false;
}
}
cur = new Node(key);//到這裡就找到了我們要插入節點的為止,我們new(key)
if (parent->_key < key)//如果父親的_key值小于我們,就連結在父節點的右邊
{
parent->_right = cur;
}
else//如果父親的_key值大于我們,就連結在父節點的左邊
{
parent->_left = cur;
}
return true;
}
bool Find(const k& key)//查找key值
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)//如果查找的值大,向右繼續查找
{
cur = cur->_right;
}
else if (cur->_key > key)//如果查找的值小,向左繼續查找
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
bool Erase(const k& key)//删除
{
Node* cur = _root;//cur向下面走,找删除節點的key值
Node* parent = nullptr;//parent就是删除節點的父節點——也就是cur的父節點
while (cur)
{
if (cur->_key < key)//老規矩,比你大向右找
{
parent = cur;//這個時候父節點要更新
cur = cur->_right;
}
else if (cur->_key > key)//比你小向左找
{
parent = cur;//同理
cur = cur->_left;
}
else到這裡就找到了
{
//三種情況:
// 1、左為空
// 2、右為空
// 3、左右都不為空,替換删除
if (cur->_left == nullptr)// 1、左為空
{
//if(parent == nullptr)
if (cur == _root)//如果是一顆單枝樹,删除根節點,parent就為空了,那麼parent->_left等操作違法
{
//這裡的cur和_root都是根節點了,我們讓_root等于_root的右節點,這樣下面删除cur,
//也就是删除原來的根節點了,而_root現在變成了原來樹的右子樹的第一個節點
_root = cur->_right;
}
else
{
if (parent->_left == cur)//如果父節點的左邊是cur,删除cur之後,cur的右子樹連結父親左邊
{
parent->_left = cur->_right;
}
else//如果父節點的右邊是cur,删除cur之後,cur的右子樹連結父親右邊
{
parent->_right = cur->_right;
}
}
delete cur;//這個時候删除就沒問題了
}
else if (cur->_right == nullptr)// 2、右為空
{
//if(parent == nullptr)
if (cur == _root)//如果是一顆單枝樹,删除根節點,parent就為空了,那麼parent->_left等操作違法
{
//這裡的cur和_root都是根節點了,我們讓_root等于_root的左節點,這樣下面删除cur,
//也就是删除原來的根節點了,而_root現在變成了原來樹的左子樹的第一個節點
_root = cur->_left;
}
else
{
if (parent->_left == cur)//如果父節點的左邊是cur,删除cur之後,cur的右左子樹連結父親左邊
{
parent->_left = cur->_left;
}
else//如果父節點的右邊是cur,删除cur之後,cur的左子樹連結父親右邊
{
parent->_right = cur->_left;
}
}
delete cur;
}
else// 3、左右都不為空,替換删除
{
//這裡parent不能初始化為nullptr,如果删除的cur是根,那麼parent就不會進入while循環進行更新
//那麼parent就一直為空,下面parent->_left等操作就非法了!!!
Node* parent = cur; //parent就是替換完之後,删除節點的父節點
Node* min = cur->_right;//min是右子樹的最小值
while (min->_left)//找右子樹最小的數,(也可以找左子樹最大的數,但是這樣太麻煩了)
//while (min && min->_left)//這裡不用這麼寫,因為前提條件就是cur删除節點左右都不為空
{
parent = min;
min = min->_left;
}
cur->_key = min->_key;//把右子樹最小值給給cur
if (parent->_left == min)//min是最左值,是以沒有左子樹,隻有右子樹需要托孤
{
parent->_left = min->_right;
}
else
{
parent->_right = min->_right;
}
delete min;
}
return true;
}
}
return false;
}
void InOrder()//樹的遞歸要使用根節點,根節點都是私有的,是以要通過嵌套的方法來調用
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node * root)//中序周遊
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;//根節點
};
void Test1()//測試用例
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (const auto& e : a)
{
t.Insert(e);
}
for (const auto& e : a)
{
t.Erase(e);
t.InOrder();
}
}
3-2、遞歸版本
BSTreeR.h:遞歸版本
這裡遞歸版本就不做過多解釋了,重要代碼都有注釋
#pragma once
///遞歸版本
template<class k>
class BSTreeNode //結構體——包含節點,左指針和右指針
{
public:
BSTreeNode<k>* _left;
BSTreeNode<k>* _right;
k _key;
BSTreeNode(const k& key)//構造函數,不寫的話new生成不了節點
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
template<class k>
class BSTreeR
{
typedef BSTreeNode<k> Node;
public:
BSTreeR()
:_root(nullptr)
{}
~BSTreeR()
{
Destory(_root);
_root = nullptr;
}
BSTreeR(BSTreeR<k>& t)
{
this->_root = Cope(t._root);
}
BSTreeR operator=(BSTreeR<k> t)
{
swap(this->_root, t._root);
return *this;
}
bool InsertR(const k& key)
{
return _InsertR(_root, key);
}
bool FindR(const k& key)
{
return _FindR(_root, key);
}
bool EraseR(const k& key)
{
return _EraseR(_root, key);
}
void InOrder()//樹的遞歸要使用根節點,根節點都是私有的,是以要通過嵌套的方法來調用
{
_InOrder(_root);
cout << endl;
}
private:
void Destory(Node* root)
{
if (root == nullptr)
return;
Destory(root->_left);
Destory(root->_right);
delete root;
}
Node* Cope(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newnode = new Node(root->_key);
newnode->_left = Cope(root->_left);
newnode->_right = Cope(root->_right);
return newnode;
}
bool _InsertR(Node*& root, const k& key)//這裡的root使用引用,這樣就不用找parent節點了,root是指針的引用
{
if (root == nullptr)//空樹直接new節點
{
root = new Node(key);
return true;
}
if (root->_key < key)
return _InsertR(root->_right, key);
else if (root->_key > key)
return _InsertR(root->_left, key);
else
return false;
}
bool _FindR(Node* root, const k& key)//查找
{
if (root == nullptr)
return false;
if (root->_key < key)
return _FindR(root->_right, key);
else if (root->_key > key)
return _FindR(root->_left, key);
else
return true;
}
bool _EraseR(Node*& root, const k& key)//root是引用,是父節點指向删除節點的指針的引用
{
if (root == nullptr)
return false;
if (root->_key < key)
return _EraseR(root->_right, key);
else if (root->_key > key)
return _EraseR(root->_left, key);
else
{
Node* dl = root;
if (root->_right == nullptr)//如果删除的節點的右子樹沒有,就讓父節點指向我的左
{
root = root->_left;
}
else if (root->_left == nullptr)//如果删除節點的左子樹沒有,就讓父節點指向我的右
{
root = root->_right;
}
else
{
Node* minright = root->_right;//找右邊最小值
while (minright->_left)
{
minright = minright->_left;
}
swap(root->_key, minright->_key);//交換兩個值
return _EraseR(root->_right, key);//交換完之後,删除的key值在右子樹的最左邊
}
delete dl;
return true;
}
}
void _InOrder(Node* root)//中序周遊
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;//根節點
};
void Test1()//測試用例
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTreeR<int> t;
for (const auto& e : a)
{
t.InsertR(e);
}
BSTreeR<int> copyt(t);
copyt.InOrder();
int b[] = { 15,54,545,4,46,65,464,84,9 };
BSTreeR<int> ax;
for (const auto& e : b)
{
ax.InsertR(e);
}
ax.InOrder();
copyt = ax;
copyt.InOrder();
for (const auto& e : a)
{
t.EraseR(e);
t.InOrder();
}
}
4、二叉搜尋樹的應用
4-1、K模型
K模型:K模型即隻有key作為關鍵碼,結構中隻需要存儲Key即可,關鍵碼即為需要搜尋到的值。
比如:給一個單詞word,判斷該單詞是否拼寫正确,具體方式如下:
·以詞庫中所有單詞集合中的每個單詞作為key,建構一棵二叉搜尋樹
·在二叉搜尋樹中檢索該單詞是否存在,存在則拼寫正确,不存在則拼寫錯誤。
4-2、KV模型
KV模型:每一個關鍵碼key,都有與之對應的值Value,即<Key, Value>的鍵值對。該種方式在現實生活中非常常見:
·比如英漢詞典就是英文與中文的對應關系,通過英文可以快速找到與其對應的中文,英文單詞與其對應的中文<word, chinese>就構成一種鍵值對;
·再比如統計單詞次數,統計成功後,給定單詞就可快速找到其出現的次數,單詞與其出現次數就是<word, count>就構成一種鍵值對。
4-3、KV模型的代碼樣例
樣例1:将我們上面的二叉搜尋樹循環版本改為KV模型
//通過一個值找另一個值key value,也就是map(下一章節講)
namespace KV
{
template<class K, class V>
struct BSTreeNode
{
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;//這裡的key值就是我們上面的key值,KV模型中,比較大小、查找等等操作都是與key值有關,但key值不能更改
V _value;//_value我們用不上,value值可以更改
BSTreeNode(const K& key, const V& value)
:_key(key)
, _value(value)
, _left(nullptr)
, _right(nullptr)
{}
};
//BSTree<string, vector<string>> 字典查找
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool insert(const K& key, const V& value)//與上面沒有什麼差别,就是參數多了個value
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* cur = _root;
Node* parent = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
void Inorder()
{
_Inorder(_root);
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
private:
void _Inorder(Node* root)
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_Inorder(root->_right);
}
Node* _root = nullptr;
};
}
樣例2:映射——通過key值找value
void Test2()
{
string arr[] = { "蘋果", "西瓜", "蘋果", "西瓜", "蘋果", "蘋果", "西瓜", "蘋果", "香蕉", "蘋果", "香蕉" };
// 詞庫中單詞都放進這個搜尋樹中
// Key的搜尋模型,判斷在不在?
// 場景:檢查單詞拼寫是否正确/車庫出入系統/...
//K::BSTree<string> dict;
// Key/Value的搜尋模型,通過Key查找或修改Value
KV::BSTree<string, string> dict;
dict.Insert("sort", "排序");
dict.Insert("string", "字元串");
dict.Insert("left", "左邊");
dict.Insert("right", "右邊");
string str;
while (cin>>str)
{
KV::BSTreeNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << ret->_value << endl;
}
else
{
cout << "無此單詞" << endl;
}
}
}
樣例3:統計水果出現次數
void TestBSTree3()
{
// 統計水果出現的次數
string arr[] = { "蘋果", "西瓜", "香蕉", "草莓","蘋果", "西瓜", "蘋果", "蘋果", "西瓜", "蘋果", "香蕉", "蘋果", "香蕉" };
KV::BSTree<string, int> countTree;
for (auto e : arr)
{
auto* ret = countTree.Find(e);
if (ret == nullptr)
{
countTree.Insert(e, 1);
}
else
{
ret->_value++;
}
}
countTree.Inorder();
}
5、二叉搜尋樹的性能分析
插入和删除操作都必須先查找,查找效率代表了二叉搜尋樹中各個操作的性能。
對有n個結點的二叉搜尋樹,若每個元素查找的機率相等,則二叉搜尋樹平均查找長度是結點在二叉搜尋樹的深度的函數,即結點越深,則比較次數越多。
但對于同一個關鍵碼集合,如果各關鍵碼插入的次序不同,可能得到不同結構的二叉搜尋樹:
最優情況下,二叉搜尋樹為完全二叉樹(或者接近完全二叉樹),其平均比較次數為: l o g 2 N log_2 N log2N
最差情況下,二叉搜尋樹退化為單支樹(或者類似單支),其平均比較次數為: N 2 \frac{N}{2} 2N
問題:如果退化成單支樹,二叉搜尋樹的性能就失去了。那能否進行改進,不論按照什麼次序插
入關鍵碼,二叉搜尋樹的性能都能達到最優?那麼我們後續章節學習的AVL樹和紅黑樹就可以上
場了。
6、總結
二叉搜尋樹隻是開胃小菜,主要就是為了下面的set和map、avl、紅黑樹等内容做鋪墊。但是也要掌握,畢竟這是為了後面的内容打基礎!