介绍
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
基本操作实现
构造函数
普通构造函数用编译期生成的就够用,我们主要实现拷贝构造函数,方便后续开发。需要注意的是:当我们自主实现一个构造函数,无论实现的是拷贝构造函数还是普通构造函数,编译器都不会帮我们再去默认生成一个构造函数(编译器生成的默认构造函数只是针对自定义类型进行初始化处理,对于自定义类型需要自己实现构造函数),此时如果我们还需要用编译器默认生成的构造函数就需要用到关键字== default == ,这是c+11之后版本才支持的。
BSTree() = default;//默认构造函数
//拷贝构造函数
BSTree(const BSTree<T>&t)
{
_proot = CopyTree(t._proot);
}
// 拷贝构造函数 现代写法
BSTree<T>& operator=(BSTree<T> t)
{
swap(_proot, t._proot);
return *this;
}
析构函数
析构函数就是释放资源 直接调用Destroy
~BSTree()
{
Destroy_Tree(_proot);
_proot = nullptr;
}
插入函数
插入有两种实现方法,分别是递归写法和非递归写法,下面想详细介绍两种写法的实现。
非递归写法
实现逻辑图:
1.插入的树为空树:直接插入
代码实现:
if (_proot == nullptr)
{
_proot = new Node(data);
return true;
}
2.非空树插入:
以之前给的示例为例,要插入的是10;
在非空树中,我们首先要找到插入的位置,根据二叉搜索树的性质,比根大的在右边,比根小的在左边,进行查找操作。代码实现如下:
pNode cur = _proot;
pNode parent =nullptr;
//找到要插入的位置
while (cur)
{
if (cur->_data<data)
{
parent = cur;
cur = cur->_pright;
}
else if (cur->_data>data)
{
parent = cur;
cur = cur->_pleft;
}
else
return false;
}
循环结束后,cur的位置就是待插入的位置,注意在此版本实现中规定不允许有相同的结点出现在二叉树,如有特殊需求可以对代码判断进行相应修改。
找到插入位置后就是进行插入操作,代码如下:
cur = new Node(data);
if (parent->_data<data)
{
parent->_pright = cur;
}
else
{
parent->_pleft = cur;
}
return true;
判断data插入哪个位置,是根节点的左边还是右边,而后插入!
递归写法
思路:传根的引用作为参数
逻辑实现
1.空树,直接插入
2.非空插入:
注意这里,在找到插入位置后,此时的root是一个空结点,同时root也是上一个结点(父节点12)的右结点,直接把data的值构造成结点就能实现插入。不需要像非递归版本一样创建一个父节点手动链接,因为插入结点就是父节点的子节点的引用,本身就是链接在一起的,只需赋值就好!
插入函数总结
无论是非递归写法还是递归写法,都各有优点,逻辑实现上,非递归的写法更让人容易理解和接受,而递归写法就比较巧妙用了引用传参,更加简洁实现。
删除函数
非递归写法
二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回;
代码实现:
//如果树是空 删除失败
if (_proot == nullptr)
{
return false;
}
//先找点
Node* pcur = _proot;
Node* parent = nullptr;
while (pcur)
{
if (data < pcur->_data)
{
parent = pcur;
pcur = pcur->_pleft;
}
else if(data>pcur->_data)
{
parent = pcur;
pcur = pcur->_pright;
}
else//找到了
否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因为bc清空都适用于a因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
代码实现:
//第一种情况 pcur节点左节点或者右节点==nullptr 这种情况也适用于pcur为叶子节点的情况
if (pcur->_pleft == nullptr)
{
if (pcur == _proot)//如果删除的是根节点
{
_proot = pcur->_pright;
}
else
{
if (pcur == parent->_pleft)//父亲左节点的左结点为空 父左结点指向子右结点
{
parent->_pleft = pcur->_pright;
}
else //父亲的右结点的左节点为空 父右指向子右
{
parent->_pright = pcur->_pright;
}
}
delete pcur;
}
else if(pcur->_pright==nullptr)
{
if (pcur == _proot) //删除根节点
{
_proot = pcur->_pleft;
}
else
{
if (pcur == parent->_pright)//pcur在父节点的右边
{
parent->_pright = pcur->_pleft;
}
else
{
parent->_pleft = pcur->_pleft;
}
}
delete pcur;
}
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:找一个替代结点 可以选择**左子树中最大结点或者右子树中最小结点,**来替换以保证搜索二叉树的基本结构不变! 这里我采用左子树中最大结点来替换
else
{
pNode ret_parent = pcur;
pNode ret = pcur->_pleft;//左子树最大值节点替换
while (ret->_pright)
{
ret_parent = ret;
ret = ret->_pright;
}
swap(pcur->_data, ret->_data);
if (ret_parent->_pleft == ret)
{
ret_parent->_pleft = ret->_pleft;
}
else
{
ret_parent->_pright = ret->_pright;
}
delete ret;
}
递归写法
递归写法核心还是用引用传参
bool _Rec_Erase(pNode& root,const T& data)
{
if (root == nullptr)
{
return false;
}
if (root->_data > data)
{
return _Rec_Erase(root->_pleft, data);
}
else if (root->_data < data)
{
return _Rec_Erase(root->_pright, data);
}
else// 找到了待删节点
{
pNode del = root;//用一个节点记录待删节点地址,否则后续操作会出现内存泄漏
//只有右边节点
if (root->_pleft == nullptr)
{
root = root->_pright;
}
//只有右边节点
else if (root->_pright == nullptr)
{
root = root->_pleft;
}
//两边都有节点 用替代法 这里用左节点最大值
else
{
Node* maxLeft = root->_pleft;
while (maxLeft->_pright)
{
maxLeft = maxLeft->_pright;
}
swap(root->_data, maxLeft->_data);
//交换完成之后递归调用删除函数,注意这里可以递归
//前面非递归写法不可以递归的原因 前面的函数交换完成之后
//根节点是整个树的根节点,而交换之后树的结构被破环了,
//根本删除不到交换后的节点,而这里给的是交换完成后,子树的根做为参数
//树的结构没有被破坏
return _Rec_Erase(root->_pleft,data);
}
delete del; //释放资源
return true;
}
总结
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的
深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
源码:
BsTree.h
#pragma ones
#include<iostream>
using namespace std;
//搜索二叉树
template<class T>
struct BSTreeNode
{
//构造函数 初始化列表
BSTreeNode(const T& data = T())
:_pright(nullptr), _pleft(nullptr), _data(data)
{}
BSTreeNode<T>* _pright;
BSTreeNode<T>* _pleft;
T _data;
};
template <class T>
class BSTree
{
typedef BSTreeNode<T>Node; //封装一下结点
typedef Node* pNode;//封装结点指针
public:
//插入函数 非递归版本
bool Insert(const T& data)
{
if (_proot == nullptr)
{
_proot = new Node(data);
return true;
}
pNode cur = _proot;
pNode parent = nullptr;
//找到要插入的位置
while (cur)
{
if (cur->_data < data)
{
parent = cur;
cur = cur->_pright;
}
else if (cur->_data > data)
{
parent = cur;
cur = cur->_pleft;
}
else
return false;
}
cur = new Node(data);
if (parent->_data < data)
{
parent->_pright = cur;
}
else
{
parent->_pleft = cur;
}
return true;
}
//插入函数的递归版本
bool Rec_Insert(const T& data)
{
return _Rec_Insert(_proot, data);
}
//删除函数 非递归版本
bool Erase(const T& data)
{
//如果树是空 删除失败
if (_proot == nullptr)
{
return false;
}
//先找点
Node* pcur = _proot;
Node* parent = nullptr;
while (pcur)
{
if (data < pcur->_data)
{
parent = pcur;
pcur = pcur->_pleft;
}
else if (data > pcur->_data)
{
parent = pcur;
pcur = pcur->_pright;
}
else//找到了
{
//第一种情况 pcur节点左节点或者右节点==nullptr 这种情况也适用于pcur为叶子节点的情况
if (pcur->_pleft == nullptr)
{
if (pcur == _proot)//如果删除的是根节点
{
_proot = pcur->_pright;
}
else
{
if (pcur == parent->_pleft)//父亲左节点的左结点为空 父左结点指向子右结点
{
parent->_pleft = pcur->_pright;
}
else //父亲的右结点的左节点为空 父右指向子右
{
parent->_pright = pcur->_pright;
}
}
delete pcur;
}
else if (pcur->_pright == nullptr)
{
if (pcur == _proot) //删除根节点
{
_proot = pcur->_pleft;
}
else
{
if (pcur == parent->_pright)//pcur在父节点的右边
{
parent->_pright = pcur->_pleft;
}
else
{
parent->_pleft = pcur->_pleft;
}
}
delete pcur;
}
//第二种情况:当前节点左右孩子都存在,
//直接删除不好删除,可以在其子树中找一个替代结点
else
{
pNode ret_parent = pcur;
pNode ret = pcur->_pleft;//左子树最大值节点替换
while (ret->_pright)
{
ret_parent = ret;
ret = ret->_pright;
}
swap(pcur->_data, ret->_data);
if (ret_parent->_pleft == ret)
{
ret_parent->_pleft = ret->_pleft;
}
else
{
ret_parent->_pright = ret->_pright;
}
delete ret;
}
return true;
}
}
return false;
}
//删除函数 递归版本
bool Rec_Erase(const T& data)
{
return _Rec_Erase(_proot, data);
}
//清空二叉树
void Destroy_Tree(pNode root)
{
if (root == nullptr)
return;
Destroy_Tree(root->_pleft);
Destroy_Tree(root->_pright);
delete root;
}
//查找是否存在
bool Find(const T& data)
{
pNode cur = _proot;
while (cur)
{
if (cur->_data < data)
{
cur = cur->_pright;
}
else if (cur->_data > data)
{
cur = cur->_pleft;
}
else
{
return true;
}
}
return false;
}
//析构函数
~BSTree()
{
Destroy_Tree(_proot);
_proot = nullptr;
}
//构造函数注释
/* 强制编译器自己生成构造 默认情况下,当代码中出现一个构造函数的时候
无论是拷贝构造还是普通的构造函数,编译器都不会生成默认的构造函数了
如果还要使用默认的构造函数(只针对内置类型有效),就要用default关键字
注意的是 default关键字是C++11版本之后的才有*/
BSTree() = default;
//拷贝构造函数
BSTree(const BSTree<T>& t)
{
_proot = CopyTree(t._proot);
}
// 拷贝构造函数 现代写法
BSTree<T>& operator=(BSTree<T> t)
{
swap(_proot, t._proot);
return *this;
}
//中序遍历 要传根节点参数 封装一下
void InOrder()
{
_InOrder(_proot);
cout << endl;
}
private:
pNode _proot;
//中序打印函数
void _InOrder(pNode root)
{
if (root == nullptr)
return;
_InOrder(root->_pleft);
cout << root->_data << " ";
_InOrder(root->_pright);
}
//递归版本的插入函数
bool _Rec_Insert(pNode& proot, const T& data)
{
if (proot == nullptr)//找到插入位置
{
proot = new Node(data);
return true;
}
if (proot->_data > data) //在节点左边插入
{
return _Rec_Insert(proot->_pleft, data);
}
else if (proot->_data < data)//在节点右边插入
{
return _Rec_Insert(proot->_pright, data);
}
return false;
}
//删除函数递归版本
bool _Rec_Erase(pNode& root, const T& data)
{
if (root == nullptr)
{
return false;
}
if (root->_data > data)
{
return _Rec_Erase(root->_pleft, data);
}
else if (root->_data < data)
{
return _Rec_Erase(root->_pright, data);
}
else// 找到了待删节点
{
pNode del = root;//用一个节点记录待删节点地址,否则后续操作会出现内存泄漏
//只有右边节点
if (root->_pleft == nullptr)
{
root = root->_pright;
}
//只有右边节点
else if (root->_pright == nullptr)
{
root = root->_pleft;
}
//两边都有节点 用替代法 这里用左节点最大值
else
{
Node* maxLeft = root->_pleft;
while (maxLeft->_pright)
{
maxLeft = maxLeft->_pright;
}
swap(root->_data, maxLeft->_data);
//交换完成之后递归调用删除函数,注意这里可以递归
//前面非递归写法不可以递归的原因 前面的函数交换完成之后
//根节点是整个树的根节点,而交换之后树的结构被破环了,
//根本删除不到交换后的节点,而这里给的是交换完成后,子树的根做为参数
//树的结构没有被破坏
return _Rec_Erase(root->_pleft, data);
}
delete del; //释放资源
return true;
}
}
};
Test.cpp
#include"BSTree.h"
void InsertTest()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto e : a)
{
t.Insert(e);
}
t.Rec_Insert(5);
t.InOrder();
}
void EraseTest()
{
BSTree<int> t;
int a[] = { 8,7,4,5,15,10,9,12,20 };
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
//删除
t.Rec_Erase(15);
t.InOrder();
t.Rec_Erase(8);
t.InOrder();
//测试Find
cout << "Find:::" << endl;
bool flag = t.Find(8);
cout << flag << endl;
bool flag2 = t.Find(12);
cout << flag2 << endl;
}
int main()
{
EraseTest();
return 0;
}
代码命名规则注释:代码中变量或者函数前 带“_’下划线的表示类的私有变量/函数。
水平有限,如有错误,敬请批评指正。