目录
一、什么是搜索二叉树
二、搜索二叉树如何删除数据
删除的是叶子结点的情况
删除的结点下面仅有一个子节点(托孤)(要删除的结点只有一个孩子)
替换法删除 (要删除的结点有两个个孩子)
三、写一棵搜索二叉树
递归版本的插入
测试代码
四、查找某一个数是否存在于我们的二叉搜索树中
测试代码
递归版本的查找
测试代码
五、二叉搜索树中删除某一个结点
测试代码
递归版本
测试代码
六、拷贝构造
析构函数
拷贝构造(深拷贝)
测试代码
赋值
测试代码
七、搜索二叉树的分析
八、Key的模型和Key/Value模型
1.英文翻译为中文
测试代码(简单英翻译中词典)
2.统计出现次数
一、什么是搜索二叉树
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
(每一棵子树都满足上面的特征)
(这样我们的搜索二叉树就非常利于我们的搜索)
比方说我们查找8,8比6大,找右子树,8比9小,找9的左子树,7比8小,找7的右子树,找到了。
最多的查找次数是树的高度!
这样就不是暴力查找了,而是利用了二叉树的特性
二叉搜索树又被称为是二叉排序树或二叉查找树。
二、搜索二叉树如何删除数据
删除的是叶子结点的情况
对于上面的二叉树,如果我们想要删除0,2,4,7,13,其实都是可以直接删除的,不会破坏我们的搜索二叉树的性质。
删除的结点下面仅有一个子节点(托孤)(要删除的结点只有一个孩子)
假如我们要删除14,由于我们的14下面只有一个13,所以我们其实可以将13和14的结点中的数据相互交换,此时我们的14也就被换到了叶子结点的位置,然后我们再直接将当前14的这个结点删除掉就可以了。
(删除的结点左为空,让父亲结点指向我的右子树,删除结点右为空,让父亲结点指向我的左子树)
其实我们上一种情况也可以归纳到我们当前这种情况,也就是我们要删除的是叶子结点的话,比方说我们要删除14下面的13,因为13的左子树为空,我们就直接将13的右子树挂载到我们的14的左子树下面,也就是nullptr,那么我们也是能够将13删掉的
替换法删除 (要删除的结点有两个个孩子)
如果我们想要删除3
(3的位置的数要满足比左子树的最大值大,比右子树的最小值小)
1.左子树的最大节点--2(左子树最右结点)
2.或者右子树的最小节点--4(右子树的最左节点)
与3进行替换,然后将3删除。
替换结点赋值给删除结点后,删除替换节点,替换节点要么没有孩子,要么只有一个孩子(如果我们上面的二叉树的4的右子树中还有一个5),可以直接删除。
三、写一棵搜索二叉树
下面是按照我们上面两个部分的思路写出来的二叉搜索树的主要实现的代码
namespace zhuyuan
{
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
//这里的k,因为我们在搜索二叉树中将我们判断的依据称之为key
template<class K>
class BSTree
{
typedef BSTreeNode<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结点往下走之前,及时更新我们的父节点
parent=cur;
cur=cur->_right;
}
else if(cur->_key>key)
{
parent=cur;
cur=cur->_left;
}
else
{
//如果这个插入的值在这棵树里面已经存在了,就会插入失败。
return false;
}
}
//cur只是一个局部变量,处理这个作用域就消失了
//所以我们先应该将其父节点挂载当前结点。所以我们在上面的代码中加入了cur的父节点,同步向下移动
cur=new Node(key);
if(parent->_key<key)
{
parent->_right=cur;
}
else
{
parent->_left=cur;
}
return true;
}
//套一层无参的去调用有参的,这样我们的_root就不会暴露给外部了。
//这里我们的类私有的函数一般都是在名称前面加上“_”,然后如果是共有的函数的话就不加
//比方是我们瞎买的呢InOrder()就是共有的,外部可以调用
//但是下面的_InOrder就是私有的,外部不可以调用
void InOrder()
{
_InOrder(_root);
}
private:
//递归必须要显式地传子树,但是我们的这里的_root又是私有的
//所以我们这里将其设置为是私有的,然后我们类内部再给出一个方法去调用中序遍历
void _InOrder(Node* root)
{
if(root== nullptr)
{
return;
}
_InOrder(root->_left);
cout<<root->_key<<" ";
_InOrder(root->_right);
}
private:
Node * _root= nullptr;
};
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
//排序+去重
t.InOrder();
}
调用测试代码
#include <iostream>
#include <string>
using namespace std;
#include "BinarySearchTree.h"
int main()
{
zhuyuan::TestBSTree1();
return 0;
}
我们观察到我们的二叉搜索树不但中序是有序的,并且还帮助我们完成了去重
递归版本的插入
//封装一层,人为将带_的方法放在private属性里面,用不带_的去调用
//下面带_其实是在private里面的
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
//思考这里的root前为啥要加上&
bool _InsertR(Node*& root, const K& key)
{
//找到了插入的位置,将我们的结点插入
if (root == nullptr)
{
//这个root仅仅是一个局部变量,是一个形参,并没有帮助我们完成插入
//还是需要将其和其父亲结点链接在一起
//但是我们这里没有父亲怎么办?
//是将这里的父亲随着二叉的链条一直往下传。
//但是我们只要在我们上面的Node*& root这里加一个引用,
//这里取引用的意义就是假设我们现在root的值是空
//但是这个root同时也是我们上一层的父节点的子节点的别名
//也就是我们直接修改的就是上一层父节点的子节点
//所以就成功实现了插入
root = new Node(key);
return true;
}
//要插入的结点的值要是比我们当前的结点的key值小,就转换到我们当前结点的左子树去插入
if (root->_key < key)
return _InsertR(root->_right, key);
//要插入的结点的值要是比我们当前的结点的key值大,就转换到我们当前结点的右子树去插入
else if (root->_key > key)
return _InsertR(root->_left, key);
//如果要插入的数据已经存在了,就不插入了,插入失败
else
return false;
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.InsertR(e);
}
t.InOrder();
cout<<endl;
}
四、查找某一个数是否存在于我们的二叉搜索树中
只要当前的值比我们的根节点数值小,那么我们就查找当前根节点的左子树,如果比我们的根节点的数值大,那么我们就查找当前根节点的右子树。
如果查找到最后找到null结点,那么就是找不到了。
bool 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 true;
}
}
//表示找不到了
return false;
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
// //排序+去重
// t.InOrder();
cout<<t.Find(10)<<endl;
cout<<t.Find(11)<<endl;
}
我们的二叉树中10是有的,11是没有的,所以分别为1和0
递归版本的查找
//封装一层,人为将带_的方法放在private属性里面,用不带_的去调用
//下面带_其实是在private里面的
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
//查找不到返回false
if (root == nullptr)
return false;
//如果我们当前root的_key小于我们的key值,我们就到我们当前结点的右子树中去查找
if (root->_key < key)
return _FindR(root->_right, key);
//如果我们当前root的_key大于我们的key值,我们就到我们当前结点的左子树中去查找
else if (root->_key > key)
return _FindR(root->_left, key);
//找到了我们的想要查找的结点
else
return true;
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
cout<<t.FindR(3)<<endl;
cout<<t.FindR(9)<<endl;
}
这里的我们的时间复杂度为O(h),这里的h为树的高度
五、二叉搜索树中删除某一个结点
这里的删除时按照我们的二中的方法进行操作的
bool Erase(const K& key)
{
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;
}
//这里的情况就是cur->_key==key
//找到了,开始尝试删除
else
{
// 开始删除
// 1、左为空
// 2、右为空
// 3、左右都不为空
//要删除的结点左为空
if (cur->_left == nullptr)
{
//如果要删除的是根节点
//当前的根节点的左子树是nullptr
//所以我们就将根节点变成我们的原先的根节点的右子树
if (cur == _root)
{
_root = cur->_right;
}
else
{
//如果我要删除的结点是父亲的左节点(要删除的结点左为空)
//那么我们就将要删除的结点的右挂载到父亲的左节点
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
//如果要删除的结点右为空
else if (cur->_right == nullptr)
{
//如果要删除的是根节点
//当前的根节点的右子树是nullptr
//所以我们就将根节点变成我们的原先的根节点的左子树
if (_root == cur)
{
_root = cur->_left;
}
else
{
//如果我要删除的结点为父亲的左节点
//将要删除的结点的左节点挂载到父亲的左节点
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
//将要删除的结点的左节点挂载到父亲的右节点
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
//如果要删除的结点左右都不为空
//替换法删除
//用右子树的最小结点
else
{
// 找到右子树最小节点进行替换
//类似于上面的写法
//我们的minParent是我们的min的父节点
//为了防止我们删除的是根节点的情况,所以我们的minParent不能初始化为nullptr
//应该初始化为cur
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
//将当前的要删除的位置的结点的值和右子树的最大值进行交换
swap(cur->_key, min->_key);
//如果我们右子树的最小值的结点是其父节点的左结点
//那么我们这里就直接进行(托孤)操作
//因为我们这里min已经是当前右子树中数值最小的结点了,那么其不可能还有左子树,因为左子树的结点会比我们的根节点要小!
//所以我们的min只可能是右子树还有结点
//和我们上面的操作方案一样,就是将min结点的右子树挂载到我们的父节点左结点上
if (minParent->_left == min)
minParent->_left = min->_right;
//如果我们的min结点是其父节点的右结点,那么我们就将我们min的右子树挂载到我们的父节点的右子树上
else
minParent->_right = min->_right;
//将我们的min结点删除掉。
delete min;
}
//删除成功
return true;
}
}
//删除失败
return false;
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
// //排序+去重
t.InOrder();
cout<<endl;
t.Erase(8);
t.InOrder();
cout<<endl;
t.Erase(3);
t.InOrder();
cout<<endl;
}
递归版本
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
//递归版本的删除
//思考这里的root前为啥要加上引用
bool _EraseR(Node*& root, const K& key)
{
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
{
//root就是我们要删除的结点
Node* del = root;
//root的左子树为空,就将当前root的右子树挂载到上层的root的子树上
if (root->_left == nullptr)
//由于我们上面的定义的时候我们的root前有&,所以我们这里
//这的root是上一层root的孩子指针的别名
//所以修改的话,就可以直接修改
//不需要再像我们的非递归版本一样再设置一个父节点用来挂载子节点
root = root->_right;
//root的右子树为空,就将当前root的左子树挂载到上层的root的子树上
else if (root->_right == nullptr)
root = root->_left;
else
{
// 找右树的最左节点替换删除
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
//交换要删除的节点的值和其右子树的值最小结点的值
swap(root->_key, min->_key);
//下面这种写法是错的
//return EraseR(key); 错的
//因为我们在交换过之后,我们原先的整一棵搜索二叉树已经不满足搜索二叉树的结构了,很可能这里就找不到了!
//(我们当前要删除的结点的值已经大于其右子树的值最小的结点的值了!)
//递归删除我们的交换过之后的结点
//因为我们当前的要删掉的位置是我们的交换过之后的原先要被删除的结点的右子树当中的最小值的结点,
//右子树的最小值的结点是不可能有左子树的,因为左子树一定会比当前的位置的节点的值小
//所以我们这里将右子树传进去
//然后这个key也就是我们原先要删除的结点的值
//交换过之后我们的这个key值也被交换到了我们的原先的结点的右子树的值最小的结点
//并且这里我们递归调用的话,我们root->right的这棵子树还是满足二叉搜索树的结构的!
//因为我们的要删除的结点原本就是要比其右子树中的值最小的结点小的!
//所以我们将要删除的结点值和其右子树最小的值进行交换值的话,我们的要删除的结点的右子树依旧满足搜索二叉树的结构!
//并且这里我们由于引用的原因,我们不用再设置一个父节点去挂载我们的子节点
//所以我们这里直接按照下面这样写就可以了
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
t.InOrder();
cout<<endl;
t.EraseR(14);
t.InOrder();
cout<<endl;
t.EraseR(13);
t.InOrder();
cout<<endl;
}
我们已经写完了增删查,但是我们的当前模型的二叉搜索树不支持改,因为一旦改了就可能不是搜索二叉树的模型了。
六、拷贝构造
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
t.InOrder();
cout<<endl;
BSTree<int> copy=t;
copy.InOrder();
}
这里其实默认是浅拷贝。
但是我们没有写析构函数。
一旦写了析构函数就会崩溃。
析构函数
~BSTree()
{
_Destory(_root);
}
void _Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
// 先销毁左节点,然后销毁右节点,最后销毁root结点,后序
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
由于两棵树在析构的时候会对同一块地址进行多次析构,所以我们的程序就会发生崩溃
拷贝构造(深拷贝)
// C++的用法:强制编译器生成默认的构造
BSTree() = default;
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
//按照树形结构去拷贝
Node* _Copy(Node* root)
{
//如果是空,就直接return空
if (root == nullptr)
{
return nullptr;
}
//插入同样的值,顺序不同我们的树的形状就会发生变化
//所以我们这里需要采用先序遍历的形式去拷贝我们的搜索二叉树
//为copy树创建新的结点
//遇到左子树拷贝左子树,遇到右子树拷贝左子树
Node* copyRoot = new Node(root->_key);
copyRoot->_left = _Copy(root->_left);
copyRoot->_right = _Copy(root->_right);
//返回拷贝的结点
return copyRoot;
}
你不写拷贝构造编译器会帮你生成默认的拷贝构造
但是你如果写了构造,那么编译器就不会再帮你生成默认的拷贝构造了!
所以我们上面代码中需要写
或者
BSTree() {}
如果不写就会产生下面的报错
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
t.InOrder();
cout<<endl;
BSTree<int> copy=t;
copy.InOrder();
}
赋值
现代式的写法,我们直接使用传值传参,t1传给t,这里的t就是t1的拷贝
我们直接将t的_root交换给我们当前this指向的t2的root就可以了
然后这个临时拷贝的t还会帮助我们去析构
(只要写完拷贝构造,我们的赋值就可以这么操作)
// t2 = t1
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
测试代码
void TestBSTree1()
{
BSTree<int> t;
int a[]={8,3,1,10,6,4,7,14,13,4,3,4,4};
for(auto e:a)
{
t.Insert(e);
}
t.InOrder();
cout<<endl;
BSTree<int> t1;
t1.Insert(3);
t1.Insert(1);
t1.Insert(2);
t1=t;
t1.InOrder();
cout<<endl;
}
七、搜索二叉树的分析
搜索二叉树的时间复杂度:O(h) h是树的高度
最坏的情况下h是N(如果我们的树都是单支的,长得就跟一根单链表一样)
所以我们的搜索二叉树还是有缺陷的!
只有接近满二叉树的形式,我们的搜索二叉树才能达到最佳的效果,也就是将我们树的高度降低到最小!
我们就需要采用AVL树来将我们的树的高度保持在 的高度
八、Key的模型和Key/Value模型
Key的模型:判断关键字在不在,也就是我们上面的模型
比方说
1、刷卡进宿舍楼
用一个文件,将这栋楼的所有的学生的学号都记录在里面
刷卡的时候,看看这个文件中有没有这个学生的学号。
这里查找的时候我们就可以是由搜索树(排序+去重)
2. 检查一篇英文文档中的单词拼写是否正确
单词是没有规律的,所以我们需要词库
我们这里就需要将词库中的单词都插入到一棵搜索树中
Key/Value的模型--通过key去找value
(允许修改这里的Value)
(比较大小还是以Key去比较的,查找的时候也是按照Key去查找的,Value不参数树的比较关系)
1.英文翻译为中文
比方说
1、简单的英文翻译中文程序
通过一个值要找到另外一个
我们的树中既要存英文,也要存中文
//在树里面将K和V绑定存到一起去
template<class K, class V>
struct BSTreeNode
{
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;
V _value;
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
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 = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
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;
}
bool Erase(const K& key)
{
//...
return true;
}
void InOrder()
{
_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 TestBSTree1()
{
//string对象也是支持比较大小的,就是按照字符串的ASC码进行比较
BSTree<string, string> dict;
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("string", "字符串");
dict.Insert("insert", "插入");
dict.Insert("erase", "删除");
string str;
//输入一个单词进行查找
//用英文查找中文
//只要有输入就会执行
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret)
{
cout << "对应的中文:" << ret->_value << endl;
}
else
{
cout << "对应的中文->无此单词" << endl;
}
}
}
如何停止这里的while
①(ctrl+z+换行)
②(ctrl+c)杀掉进程
2.统计出现次数
比方说我们统计水果出现的个数
如果只是使用计数排序的话,我们不能找到字符串
但是我们之前写的KV模型的树可以做到!
void TestBSTree2()
{
string arr[] = { "香蕉", "苹果", "香蕉", "草莓", "香蕉", "苹果", "苹果", "苹果" };
BSTree<string, int> countTree;
for (auto& str : arr)
{
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret)
{
ret->_value++;
}
else
{
countTree.Insert(str, 1);
}
}
countTree.InOrder();
}
这里的排序是按照苹果,草莓,香蕉去排序的。
我们链表相交和复杂链表的复制就能有新的策略去做
链表相交:
我们想要去查找最先的公共节点的话,我们就先将L1遍历一遍,然后将其地址放入一棵二叉搜索树当中,然后遍历L2的时候,每经过一个结点就查找一个在不在L1中,最先找到的就是我们的最先的公共节点。
复杂链表的赋值:
也就是我们的每一个链表的结点都是有一个随机指针结点,指向不同的链表位置的。
然后我们原先的写法就是在每一个链表结点后面都挂载一个拷贝的结点,然后通过一定的方式在拷贝完成之后将其剪下来。
但是有了搜索二叉树了之后,我们就可以存储原结点地址和拷贝结点的映射的Key/Value模型。
不再像以前那么繁琐了