目录
一.前言
二叉搜索树是在C++中一个很重要的数据结构,学好二叉搜索树为后面的红黑树、Map和Set容器做铺垫,所以我们这篇博客主要是用来讲二叉搜索树的基本结构和应用.
二.二叉搜索树的概念
所有的根节点大于左子树的节点,小于右子树的节点的二叉树就叫做二叉搜索树。
二叉搜索的性质:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
二叉搜索树的搜索时间复杂度:
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
搜索效率最优的情况下:完全二叉树,其一次搜索的平均比较的次数是O(logN)
搜索效率最坏的情况:单支二叉树,其一次搜索的平均的比较次数是O(N)
而搜索二叉树的搜索的时间复杂度是O(N),其是按照最坏的情况进行计算的
三.二叉搜索树的构建
3.1 树节点的构建
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
//建立搜索二叉树的构造函数
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{
}
};
3.2 搜索二叉树的基本结构
template<class K>
class BSTree
{
public
typedef BSTreeNode<K> Node;
BinarySearchTree()
:_root(nullptr)
{
}
.......
private:
//为什么定义Node变量?
//1.插入删除时,好判断该节点是否为空
//2.节点的左右孩子都是指针,比较方便赋值
Node* _root;
};
3.3 插入数据
1.如果插入的是一个空树,则直接进行插入,返回true即可
2.树不为空的话,则按照二叉搜索树的性质进行插入节点,遍历二叉搜索树找到对应的节点值并进行插入
3.如果插入的数据在树中已经存在的话,就不会将数据插入到树中,并返回false.在二叉搜索树数据都是唯一的.
插入数据代码实现:
非递归版本
bool _Insert(const K& key)
{
//1.如果插入的值是个空树的话
//创建新的节点,并进行插入
if(_root==nullptr)
{
_root=new Node(key);
return true;
}
//2.遍历二叉搜索树找节点
Node* cur=_root;
Node* parent=nullptr;
while(cur)
{
if(cur->_key<key)
{
//插入的节点值要比当前的节点值大
//往树的右子树进行遍历
parent=cur;
cur=cur->_key;
}
else if(cur->_key>key)
{
//如果插入的节点值要比当前的节点值小
//往树的左子树进行遍历
parent=cur;
cur=cur->_left;
}
else
{
//这里就是说明有相同的节点
//因为二叉搜索树的性质
//这里就不能插入值
return false;
}
}
//找到这个节点位置,那就要建立相应的关系
cur=new Node(key);
//就是看cur关联到parent的左节点还是右节点
if(parent->_key>key)
{
//这样就关联左节点
parent->_left=cur;
}
else
{
//关联右节点
parent->_right=cur;
}
//最后这样就算结束了
return true;//插入完成
}
递归版本主要就是利用递归来找到该插入的节点位置,节点建立关联的部分是一致的
//递归版本的插入
bool _InsertR(Node* &root,const K& key)
{
//如果节点是空,则就要创建节点给它
if(root ==nullptr)
{
root=new Node(key);
}
if(root->_key > key)
{
//往左边进行插入
return _InsertR(root->_left,key);
}
else if(root->_key < key)
{
//往节点右边进行插入
return _InsertR(root->_right,key);
}
else
{
//这块就是碰到相等的节点了
return false;
}
}
3.4 查找数据节点
//搜索二叉树的查找
bool Find(Node* _root,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 true;
}
}
return false;
}
3.5 删除数据节点
当我们要进行删除二叉搜索树中的某个节点时,删除后的二叉搜索树仍要保持搜索树的性质,故删除可能会有以下四种情况:
1.要删除的节点没有孩子节点.例如17.
删除这个节点,直接delete即可.
2.要删除的节点只有左节点.例如5
先进行遍历找到该值的节点值,然后在进行判断孩子节点是连接在父亲的左边还是右边,最后再进行连接.
3.要删除有右孩子节点的节点.例如15
通过遍历,找到该值之后,将有孩子的节点链接到父节点上.
4.要删除的节点有左右孩子的节点,比如7,13,10
如果直接删除这样的节点的话,就会让使它的左右子树没有根节点,并且还要保持搜索二叉树的性质?
可以找出左子树的最大节点或者是右子树的最小节点,然后去替换根节点,并将替换的节点给删除掉.
删除这个7的过程,找出左子树的最大值,并进行替换之后重新关联在一起.
删除10根节点时,找出左子树的最大值,并进行替换.
代码:
bool _erase(const T key)
{
if(_root==nullptr)
{
//如果是空树,肯定找不到
return false;
}
//接下来就是找节点
Node* cur=_root;
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
{
//在这里就是找到了
break;
}
}
//删除节点的三种情况
//1.删除只有右孩子的节点
if(cur->_left==nullptr)
{
if(parent==nullptr)
{
//说明删除的就是根节点
_root=cur->_right;
delete cur;
return true;
}
if(parent->_left==cur)
{
//然后就是把节点给关联到一起
parent->_left=cur->_right;
}
else if(parent->_right==cur)
{
parent->_right=cur->right;
}
//把节点给删除掉
delete cur;
}
//删除只有左孩子的节点
else if(cur->_right ==nullptr)
{
if(parent==nullptr)
{
_root=cur->_left;
delete cur;
return true;
}
if(parent->_left==cur)
{
parent->_left=cur->_left;
}
else if(parent->_right==cur)
{
parent->_right=cur->_left;
}
delete cur;
}
else
{
//删除有左右孩子节点
//找到左子树的最大节点并进行替换
Node* leftmax=cur->_left;
Node* leftmaxparent=cur;//记录下左子树的最大节点
//找出左子树的最大节点
//非递归版本用while循环就可以
while(leftmax->_right)
{
leftmaxparent =leftmax;
leftmax=leftmax->right;
}
//在这块其实我想删除的是这个根节点
cur->_key=leftmax->_key;
//删除节点并建立关联
if(leftmaxparent->_left ==leftmax)
{
leftmaxparent->_left =leftmax->_left;
}
else
{
leftmaxparent->_right=leftmax->_left;
}
//删除节点
delete leftmax;
return fasle;
}
//遍历完之后就是没找到
return false;
}
假如要删除根节点的关联示意图如下:
递归版本代码:
递归版本的代码主要就是在找这个节点的过程是递归,其他的过程都是一致.
bool _eraseR(Node* &root,const K key)
{
//查找key相对应的值
if(root==nullptr)
{
//说明这就是个空树,没办法删除节点
return false;
}
if(root->_key < key)
{
//那就往右子树遍历
_eraseR(root->_right,key);
}
else if(root->_key > key)
{
//那就往左子树进行遍历
_eraseR(root->_left,key);
}
else
{
//这里就证明找到了
if(root->_left==nullptr)
{
Node* tmp=root;
//这里有点难理解,我们等下进行调试观察
root=root->_right;
delete tmp;
}
else if(root->_right ==nullptr)
{
Node* tmp=root;
//同上
root=root->_left;
delete tmp;
}
else
{
//下面就是第三种情况
//左右孩子均存在
//找左子树的最大节点
Node* leftmax=root->left;
Node* leftmaxparent=root;
while(leftmax->_right)
{
leftmaxparent=leftmax;
leftmax=leftmax->_right;
}
//赋值给根节点
root->_key=leftmax->_key;
//删除节点
if(leftmaxparent->_left==leftmax)
{
leftmaxparent->_left=leftmax->_left;
}
else
{
leftmaxparent->_right=leftmax->_left;
}
//删除节点
delete leftmax;
return true;
}
}
//运行到这里说明没有要删除的节点
return false;
}
下面是我个人的一些调试技巧和解释一下删除只有左孩子节点和右孩子节点的删除情况.
假如我们要构建一棵这样的单支链树,应该怎么插入值,第一个节点的插入值是根节点,然后插入值不断跟根节点进行比较,判断是将其链接到左子树还是右子树.
void TestBSTree1()
{
//新建一棵树
BSTree<int>t;
int a[] = { 10,6,5,3 };
//数据测试
for (auto e : a)
{
t.Insert(e);
}
//排序+去重
t.InOrder();
//在下面这句话中打一个断点
t.EraseR(6);
}
假如我们要进行删除6这个节点,我们就进行调试观察一下.
第一步:首先进来的是根节点,并进行遍历找到6这个节点
下面就是找左节点,并成功找到6这个节点.
接下来的这一步很绕,希望大家能够理解.
这里可以这样进行理解:root=_root->left;root就是_root->left的一个别名
四.二叉搜索树的应用
1.K模型
key模型只有key作为关键码,也就是定义树的节点只需要存_key一个就可以,关键码即为搜索到的值.我们上面示例的二叉搜索树就是k模型,因为树的节点只有一个key可以进行存储数据.
2. key-value模型
每一个关键key值都对应有一个value值,也就是说树的节点存在两个值,构成一个键值对,我们可以通过对key的查找找到对应的节点,然后将相应的value给映射出来,也就是说我们在进行增删查(二叉搜索树不能够随意的改动)也是按照Key进行增删查,只不过咋创建节点的时候,增加了一个value值.
用Key-Value模型利用二叉搜索树来写一个简单的中英互译的字典,用英文作为key值,中文作为value值.
void TestBSTree1()
{
BSTree<string, string> dict;
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("string", "字符串");
dict.Insert("insert", "插入");
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << "对应的中文:" << ret->_value << endl;
}
else
{
cout << "对应的中文->无此单词" << endl;
}
}
}
好了,这就是对二叉搜索树的理解,限于笔者的能力有限,如出现疏漏,请多多指教!