c++进阶之----二叉搜索树
一、概念与性质
二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树结构,具有以下性质:
1)若任意节点的左子树不为空,则左子树上所有节点的值均小于它的根节点的值。
2)若任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值。
3)任意节点的左子树、右子树均为二叉搜索树。
二、二叉搜索树的特性
1. 中序遍历有序
对 BST 进行中序遍历(左 → 根 → 右),结果是一个升序序列。
- 示例中的树中序遍历结果:`1, 3, 4, 6, 7, 8, 10, 13, 14`
2.性能分析
3. 高效查找:利用有序性,每次比较可排除一半子树。
4. 动态操作:支持插入、删除、查找操作,时间复杂度与树的高度相关。
三、代码详解
1.树的创建
大致思路和我们之前创建二叉树是一样的,先造结点,之后再组装成树,只不过在这里我们没有再写一个初始化函数的必要了,因为我们可以利用c++的初始化列表在造结点的时候变完成初始化!具体代码如下:
template<class k>
struct BSTNode
{
k _key;
BSTNode<k>* _left;
BSTNode<k>* _right;
BSTNode(const k& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
};
template<class k>
class BSTree
{
typedef BSTNode<k> Node;
public:
private:
Node* _root = nullptr;
};
2.中序遍历
这里和之前二叉树中序遍历代码基本一样,只不过我们将其进行了一下封装,详见代码注释!
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//由于我们之后要遍历这棵树,还要传参,很麻烦,而且_root还是私有成员,我们不如在把这个函数封装一下,利于调用
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
3.插入功能的实现
由搜索二叉树的概念及性质我们知道,对于任意一个节点X来说,其左子树任意结点的值要小于X结点的值,其右子树任意的结点值要大于结点X,我们可以用这个性质进行插入功能的实现。具体方法如下:
1)首先确定要在什么位置插入,由于这是树状结构,如果我们只用一个指针的话即便找到位置也无法插入,因为指针不能倒着走回去,所以在这里我们可以借用之前的双指针法,依次定位追踪父节点和子节点,在结合上文所说的,大于当前结点向右看,小于向左看,从而确定插入的位置
2)找到插入的位置之后,直接将要插入的数据打包成结点,并根据BSTree的性质,判断插在左子树还是右子树
注:插入值跟当前结点相等的值,可以往右走,也可以往左走,找到空位置,插入新结点。
(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走)
代码实现如下:
bool Insert(const k& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _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这个位置插入结点
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
测试一下:
void test1()
{
int a[]= { 8, 3, 1, 10, 1, 6, 4, 7, 14, 13 };
BSTree<int> bst;
for (auto e : a)
{
bst.Insert(e);
}
bst.InOrder();
}
int main()
{
test1(); //输出1 3 4 6 7 8 10 13 14
return 0;
}
4.查找功能的实现
查找功能的代码较为简单,利用BStree的性质遍历即可,代码如下:
void Find(const k& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
5.删除功能的实现
要删除的结点X分为如下几种情况:
1)X的左右孩子均为空
2)X的左右孩子中有一个为空
3)X的左右孩子均不为空
解决方法如下:首先找到要删除的结点,
若为第一种情况,则直接删除
若为第二种情况,下一步要判断结点X是左孩子还是右孩子,并将X的父节点与X的子节点链接起来
若为第三种情况, 由于直接删的代价太大,我们还是借助堆的删除的方法,交换法,找左子树的最大结点(最右结点)或者右子树的最小结点(最左结点),将二者交换,之后准备删除工作,首先要判断的是X结点在其父节点的哪一侧,(具体原因详见代码)之后删除结点,并将其父节点与X的子节点连接起来即可
bool Erase(const k& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//开始删除
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_right)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else
{
// 找右子树的最小节点(最左节点)替代
Node* replaceparnet = cur;
Node* replace = cur->_right;
//找最左节点
while (replace->_left)
{
replaceparnet = replace;
replace = replace->_left;
}
swap(cur->_key, replace->_key);
//此时还要判断一下replace在哪一侧,因为假如replace在右,而parent还有左孩子
//此时直接改会导致原来的子树失联
if (replaceparnet->_left == replace)
{
//由于此时replace已经是最左结点(其没有左孩子了,但是右孩子不确定),
// 所以parent的左孩子和replace的右孩子建立关系
replaceparnet->_left = replace->_right;
}
else
{
//解释同上,确定好左右关系即可
replaceparnet->_right = replace->_right;
}
delete replace;
}
return true;
}
}
return false;
}
6.总体代码汇总
这是bstree.h文件
#pragma once
#include<iostream>
using namespace std;
template<class k>
struct BSTNode
{
k _key;
BSTNode<k>* _left;
BSTNode<k>* _right;
BSTNode(const k& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
};
template<class k>
class BSTree
{
typedef BSTNode<k> Node;
public:
bool Insert(const k& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _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这个位置插入结点
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//由于我们之后要遍历这棵树,还要传参,而且_root还是私有成员,我们不如在把这个函数封装一下
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
void Find(const k& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
bool Erase(const k& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//开始删除
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_right)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else
{
// 找右子树的最小节点(最左节点)替代
Node* replaceparnet = cur;
Node* replace = cur->_right;
//找最左节点
while (replace->_left)
{
replaceparnet = replace;
replace = replace->_left;
}
swap(cur->_key, replace->_key);
//此时还要判断一下replace在哪一侧,因为假如replace在右,而parent还有左孩子
//此时直接改会导致原来的子树失联
if (replaceparnet->_left == replace)
{
//由于此时replace已经是最左结点(其没有左孩子了,但是右孩子不确定),
// 所以parent的左孩子和replace的右孩子建立关系
replaceparnet->_left = replace->_right;
}
else
{
//解释同上,确定好左右关系即可
replaceparnet->_right = replace->_right;
}
delete replace;
}
return true;
}
}
return false;
}
private:
Node* _root = nullptr;
};
这是Test.cpp文件
#include "bstree.h"
void test1()
{
int a[]= { 8, 3, 1, 10, 1, 6, 4, 7, 14, 13 };
BSTree<int> bst;
for (auto e : a)
{
bst.Insert(e);
}
bst.InOrder();
bst.Insert(18);
bst.InOrder();
bst.Insert(5);
bst.InOrder();
bst.Insert(0);
bst.InOrder();
bst.Erase(18);
bst.InOrder();
bst.Erase(0);
bst.InOrder();
bst.Erase(5);
bst.InOrder();
for (auto e : a)
{
bst.Erase(e);
bst.InOrder();
}
}
int main()
{
test1();
return 0;
}
7.扩展
在现实中,我们可能很少遇到结点变量只有一个的情况,比如我们想做一个英汉搜索字典,或者统计一下某个单词或字符出现了多少次,那我们便可以加一个模板参数,具体见代码!
这是.h文件
namespace key_value
{
template<class k,class v>
struct BSTNode
{
k _key;
v _value;
BSTNode<k,v>* _left;
BSTNode<k,v>* _right;
BSTNode(const k& key,const v& value)
:_key(key)
,_value(value)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class k,class v>
class BSTree
{
typedef BSTNode<k,v> Node;
public:
~BSTree()
{
Destory(_root);
_root = nullptr;
}
bool Insert(const k& key,const v& value)
{
if (_root == nullptr)
{
_root = new Node(key,value);
return true;
}
Node* parent = nullptr;
Node* cur = _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这个位置插入结点
cur = new Node(key,value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//由于我们之后要遍历这棵树,还要传参,而且_root还是私有成员,我们不如在把这个函数封装一下
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " " << root->_value << endl;
_InOrder(root->_right);
}
Node* Find(const k& key)
{
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)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//开始删除
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_right)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else
{
// 找右子树的最小节点(最左节点)替代
Node* replaceparnet = cur;
Node* replace = cur->_right;
//找最左节点
while (replace->_left)
{
replaceparnet = replace;
replace = replace->_left;
}
swap(cur->_key, replace->_key);
swap(cur->_value, replace->_key);
//此时还要判断一下replace在哪一侧,因为假如replace在右,而parent还有左孩子
//此时直接改会导致原来的子树失联
if (replaceparnet->_left == replace)
{
//由于此时replace已经是最左结点(其没有左孩子了,但是右孩子不确定),
// 所以parent的左孩子和replace的右孩子建立关系
replaceparnet->_left = replace->_right;
}
else
{
//解释同上,确定好左右关系即可
replaceparnet->_right = replace->_right;
}
delete replace;
}
return true;
}
}
return false;
}
void Destory(Node* root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
}
private:
Node* _root = nullptr;
};
}
这是英汉词典和统计次数的示例
void test2()
{
using namespace key_value;
key_value::BSTree<string, string> dict;
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("insert", "插入");
dict.Insert("string", "字符串");
//查字典
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret)
{
cout << "->" << ret->_value << endl;
}
else
{
cout << "无此单词,请重新输入" << endl;
}
}
//数水果
string arr[] = { "苹果", "西瓜", "苹果","苹果","苹果", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
key_value::BSTree<string, int> countTree;
for (auto& e : arr)
{
BSTNode<string, int>* ret = countTree.Find(e);
if (ret == nullptr)
{
countTree.Insert(e, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
这是测试结果