前言介绍
二叉搜索树(Binary Search Tree)顾名思义,是一个为搜索效率而生的数据结构,那接下来我们就先了解二叉搜索树的概念及如何实现。
一、概念
二叉搜索树别名:二叉排序树 / 二叉查找树,它可以是一棵空树,或者是具备以下性质的树:
【非空二叉搜索树必须具备以下性质】
1. 左子树不为空,左子树的所有节点值 < 根结点值
2. 右子树不为空,右子树的所有节点值 > 根结点值
图形实例:(必须严格满足性质)
二、模拟实现
2.1 创造树的节点及树的结构
//思考:创建一个二叉搜索树,首先要构造树的节点,其次再实现树的结构
//创建树的节点时,必须思考节点内部应该包含哪些变量?
// 1. 节点代表的值,2. 节点指向左右子树的指针,3. 指向父亲节点的指针
//开始构建树的节点,我们希望节点内部的成员可以被外界使用,所以使用struct类
template <class V>
struct BSTreeNode
{
BSTreeNode(const V& val)
: _val(val)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
V _val;
BSTreeNode<V>* _left;
BSTreeNode<V>* _right;
BSTreeNode<V>* _parent;
};
//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V>
class BSTree
{
public:
typedef BSTreeNode<V> Node;
//插入
bool insert(const V& val);
//删除
bool erase();
//查找
Node* find(const V& val);
//中序遍历 -> 有序
void inorder();
private:
Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
};
2.2 编写成员函数
(1)插入insert
按照二叉搜索树的性质来插入即可,需要注意的是下面代码不允许插入相同值
bool insert(const V& val)
{
//空树
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
//非空树
else
{
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else if(cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else
{
cout << "不允许插入已有的数据" << endl;
return false;
}
}
//已经找到插入节点的父亲节点,开始判断插入左边还是右边
Node* newnode = new Node(val);
// 比根小,去左边插入
if(parent->_val > val)
{
parent->_left = newnode;
}
// 比根大,去右边插入
else
{
parent->_right = newnode;
}
newnode->_parent = parent;
return true;
}
}
(2)查找find
按照二叉搜索树的规则来查找,会发现最坏情况下只需要查找高度次,后面有测效率的代码
Node* find(const V& val)
{
Node* cur = _root;
while(cur)
{
if(cur->_val > val)
{
cur = cur->_left;
}
else if(cur->_val < val)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
(3)中序遍历 inorder
只有中序遍历二叉搜索树才是有序,所以这里只使用中序遍历
void inorder()
{
_inorder(_root);
cout << endl;
}
void _inorder(Node* root) //像这种被封装一层的函数,我们一般不会对外开放,所以要放到private里
{
if(root == nullptr)
return;
_inorder(root->_left);
cout << root->_val << " ";
_inorder(root->_right);
}
(4)删除erase【重点】
在删除节点这里,是会影响二叉搜索树的性质的,所以我们要分下面4种情况来讨论:
情况1:被删除的节点不存在,直接返回false
情况2:被删除的节点存在,但为叶节点,则直接删除,不会影响二叉搜索树
情况3:被删除的节点的只有左孩子
情况4:被删除的节点的只有右孩子
情况2、3、4可以合并解决:
因为我们会发现,情况3、4只是将节点删除,再连接父节点和孩子节点即可,基本对树的结构也无影响,所以情况2、3、4可以合并解决
情况5:被删除的节点有左右孩子
当被删除节点有左右孩子节点的时候,如下图:
在删除17后,我们如何连接父亲节点和孩子节点?所以直接删除17,一定会影响二叉搜索树,那我们就要去寻找删除之后不影响整棵树,且要保证是二叉搜索树的节点。
如何寻找?
先思考:删除哪个节点不影响二叉搜索树?一定是叶子节点,删除叶子节点一定不会影响这棵树,且还可以保证删除后仍为二叉搜索树。
删除哪个叶子节点?
首先,当前节点是大于左子树的所有节点,小于右子树的所有节点,那我们就要找一个满足这两个要求的叶子节点。
通过观察符合要求的叶子节点为:
1. 左子树的最右节点
2. 右子树的最左节点
所以我们无论将哪一个节点的值与del_node交换,再删除该节点,都符合我们的要求
方法1:与左子树的最右节点交换
方法2:与右子树的最左节点交换
代码实现:
注意:这里别忘了考虑删除节点为根结点的情况哦~
//删除
bool erase(const V& val)
{
Node* del_node = find(val);
//1. 被删除节点不存在
if(del_node == nullptr)
return false;
//2. 被删除节点只有左孩子/只有右孩子/左右孩子都没有
else if(del_node->_left == nullptr || del_node->_right == nullptr)
{
//删除节点为根结点
if(del_node == _root)
{
_root = del_node->_left == nullptr ?
del_node->_right : del_node->_left;
delete del_node;
return true;
}
//不为根结点
Node* parent = del_node->_parent;
//只有左孩子
if(del_node->_left != nullptr)
{
if(parent->_left == del_node)
{
parent->_left = del_node->_left;
}
else
{
parent->_right = del_node->_left;
}
del_node->_left->_parent = parent;
}
//只有右孩子
else if(del_node->_right != nullptr)
{
if(parent->_left == del_node)
{
parent->_left = del_node->_right;
}
else
{
parent->_right = del_node->_right;
}
del_node->_right->_parent = parent;
}
//叶节点,因为都要删除,所以就放在一起删除了
delete del_node;
del_node = nullptr;
}
//3. 被删除节点有左右孩子
else
{
//这里只实现找左子树的最右节点,另外的方法大家自行实现
Node* find_LT_right = del_node->_left;
while(find_LT_right->_right)
{
find_LT_right = find_LT_right->_right;
}
//交换值
swap(find_LT_right->_val, del_node->_val);
//删除叶节点
//删除之前,要让其父节点的被删除孩子指向空
//这里不是父节点的右孩子原因:左子树只有一个节点
find_LT_right == find_LT_right->_parent->_left ?
find_LT_right->_parent->_left = nullptr
: find_LT_right->_parent->_right = nullptr;
delete find_LT_right;
find_LT_right = nullptr;
}
return true;
}
三、代码汇总
#include <iostream>
#include <cassert>
using namespace std;
//思考:创建一个二叉搜索树,首先要构造树的节点,其次再实现树的结构
//创建树的节点时,必须思考节点内部应该包含哪些变量?
// 1. 节点代表的值,2. 节点指向左右子树的指针,3. 指向父亲节点的指针
//开始构建树的节点,我们希望节点内部的成员可以被外界使用,所以使用struct类
template <class V>
struct BSTreeNode
{
BSTreeNode(const V& val)
: _val(val)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
V _val;
BSTreeNode<V>* _left;
BSTreeNode<V>* _right;
BSTreeNode<V>* _parent;
};
//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V>
class BSTree
{
public:
typedef BSTreeNode<V> Node;
//插入
bool insert(const V& val)
{
//空树
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
//非空树
else
{
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else if(cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else
{
cout << "不允许插入已有的数据" << endl;
return false;
}
}
//已经找到插入节点的父亲节点,开始判断插入左边还是右边
Node* newnode = new Node(val);
// 比根小,去左边插入
if(parent->_val > val)
{
parent->_left = newnode;
}
// 比根大,去右边插入
else
{
parent->_right = newnode;
}
newnode->_parent = parent;
return true;
}
}
//删除
bool erase(const V& val)
{
//没有节点了
assert(size() > 0);
Node* del_node = find(val);
//1. 被删除节点不存在
if(del_node == nullptr)
return false;
//2. 被删除节点只有左孩子/只有右孩子/左右孩子都没有
else if(del_node->_left == nullptr || del_node->_right == nullptr)
{
//删除节点为根结点
if(del_node == _root)
{
_root = del_node->_left == nullptr ? del_node->_right : del_node->_left;
delete del_node;
return true;
}
//不为根结点
Node* parent = del_node->_parent;
//只有左孩子
if(del_node->_left != nullptr)
{
if(parent->_left == del_node)
{
parent->_left = del_node->_left;
}
else
{
parent->_right = del_node->_left;
}
del_node->_left->_parent = parent;
}
//只有右孩子
else if(del_node->_right != nullptr)
{
if(parent->_left == del_node)
{
parent->_left = del_node->_right;
}
else
{
parent->_right = del_node->_right;
}
del_node->_right->_parent = parent;
}
//为叶节点,要处理叶节点的父亲节点被删除的孩子置为空
else
{
parent->_left == del_node ?
parent->_left = nullptr:
parent->_right = nullptr;
}
delete del_node;
del_node = nullptr;
}
//3. 被删除节点有左右孩子
else
{
//这里只实现找左子树的最右节点,另外的方法大家自行实现
Node* find_LT_right = del_node->_left;
while(find_LT_right->_right)
{
find_LT_right = find_LT_right->_right;
}
//交换值
swap(find_LT_right->_val, del_node->_val);
//删除叶节点
//删除之前,要让其父节点的被删除孩子指向空
//这里不是父节点的右孩子原因:左子树只有一个节点
find_LT_right == find_LT_right->_parent->_left ?
find_LT_right->_parent->_left = nullptr
: find_LT_right->_parent->_right = nullptr;
delete find_LT_right;
find_LT_right = nullptr;
}
return true;
}
//查找
Node* find(const V& val)
{
Node* cur = _root;
while(cur)
{
if(cur->_val > val)
{
cur = cur->_left;
}
else if(cur->_val < val)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
//中序遍历 -》 有序
void inorder()
{
_inorder(_root);
cout << endl;
}
//节点个数
size_t size()
{
return _size(_root);
}
//求高度
size_t height()
{
return _height(_root);
}
private:
Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
void _inorder(Node* root)
{
if(root == nullptr)
return;
_inorder(root->_left);
cout << root->_val << " ";
_inorder(root->_right);
}
size_t _size(Node* root)
{
if(root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
size_t _height(Node* root)
{
if(root == nullptr)
return 0;
//求左子树的高度
size_t left = _height(root->_left);
//求右子树的高度
size_t right = _height(root->_right);
//返回左右子树最大的高度,作为树的高度
return left > right ? left + 1 : right + 1;
}
};
四、二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
4.1 不足
1. 这里我们只能传整型、字符型、浮点型的数据,无法传入字符串、pair或其他自定义类型的数据
2. 当插入的数据是递增或者递减时,会退化成单支树的情况,使得查找的效率退化为O(n)
4.2 优化
优化1:
使用仿函数,并作为模版参数,来使string可以作为节点的值;
但在pair这里,则在模拟实现map、set时介绍给大家,就是多个封装。
改动后代码:
//使用仿函数解决问题
template <class V>
class Comp
{
public:
bool operator()(const V& v1, const V& v2)
{
return v1 < v2;
}
};
template<>
class Comp<string>
{
public:
bool operator()(const string& s1, const string& s2)
{
return s1 > s2;
}
};
//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V, class Comp = Comp<V>> ///******/
class BSTree
{
public:
typedef BSTreeNode<V> Node;
//插入
bool insert(const V& val)
{
//空树
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
//非空树
else
{
Comp comp; ///*****
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(comp(val, cur->_val))
{
parent = cur;
cur = cur->_left;
}
else if(comp(cur->_val, val))
{
parent = cur;
cur = cur->_right;
}
else
{
cout << "不允许插入已有的数据" << endl;
return false;
}
}
//已经找到插入节点的父亲节点,开始判断插入左边还是右边
Node* newnode = new Node(val);
// 比根小,去左边插入
if(comp(val, parent->_val))
{
parent->_left = newnode;
}
// 比根大,去右边插入
else
{
parent->_right = newnode;
}
newnode->_parent = parent;
return true;
}
}
//查找
Node* find(const V& val)
{
Comp comp;
Node* cur = _root;
while(cur)
{
if(comp(val, cur->_val))
{
cur = cur->_left;
}
else if(comp(cur->_val, val))
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
private:
Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
};
优化2:
使用AVL树或者红黑树来优化查找效率,解决单支树的情况:
五、优化后代码汇总
#include <iostream>
#include <cassert>
using namespace std;
//思考:创建一个二叉搜索树,首先要构造树的节点,其次再实现树的结构
//创建树的节点时,必须思考节点内部应该包含哪些变量?
// 1. 节点代表的值,2. 节点指向左右子树的指针,3. 指向父亲节点的指针
//开始构建树的节点,我们希望节点内部的成员可以被外界使用,所以使用struct类
template <class V>
struct BSTreeNode
{
BSTreeNode(const V& val)
: _val(val)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
V _val;
BSTreeNode<V>* _left;
BSTreeNode<V>* _right;
BSTreeNode<V>* _parent;
};
template <class V>
class Comp
{
public:
bool operator()(const V& v1, const V& v2)
{
return v1 < v2;
}
};
template<>
class Comp<string>
{
public:
bool operator()(const string& s1, const string& s2)
{
return s1 > s2;
}
};
//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V, class Comp = Comp<V>>
class BSTree
{
public:
typedef BSTreeNode<V> Node;
//插入
bool insert(const V& val)
{
//空树
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
//非空树
else
{
Comp comp;
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(comp(val, cur->_val))
{
parent = cur;
cur = cur->_left;
}
else if(comp(cur->_val, val))
{
parent = cur;
cur = cur->_right;
}
else
{
cout << "不允许插入已有的数据" << endl;
return false;
}
}
//已经找到插入节点的父亲节点,开始判断插入左边还是右边
Node* newnode = new Node(val);
// 比根小,去左边插入
if(comp(val, parent->_val))
{
parent->_left = newnode;
}
// 比根大,去右边插入
else
{
parent->_right = newnode;
}
newnode->_parent = parent;
return true;
}
}
//删除
bool erase(const V& val)
{
//没有节点了
assert(size() > 0);
Node* del_node = find(val);
//1. 被删除节点不存在
if(del_node == nullptr)
return false;
//2. 被删除节点只有左孩子/只有右孩子/左右孩子都没有
else if(del_node->_left == nullptr || del_node->_right == nullptr)
{
//删除节点为根结点
if(del_node == _root)
{
_root = del_node->_left == nullptr ? del_node->_right : del_node->_left;
delete del_node;
return true;
}
//不为根结点
Node* parent = del_node->_parent;
//只有左孩子
if(del_node->_left != nullptr)
{
if(parent->_left == del_node)
{
parent->_left = del_node->_left;
}
else
{
parent->_right = del_node->_left;
}
del_node->_left->_parent = parent;
}
//只有右孩子
else if(del_node->_right != nullptr)
{
if(parent->_left == del_node)
{
parent->_left = del_node->_right;
}
else
{
parent->_right = del_node->_right;
}
del_node->_right->_parent = parent;
}
//为叶节点,要处理叶节点的父亲节点被删除的孩子置为空
else
{
parent->_left == del_node ?
parent->_left = nullptr:
parent->_right = nullptr;
}
delete del_node;
del_node = nullptr;
}
//3. 被删除节点有左右孩子
else
{
//这里只实现找左子树的最右节点,另外的方法大家自行实现
Node* find_LT_right = del_node->_left;
while(find_LT_right->_right)
{
find_LT_right = find_LT_right->_right;
}
//交换值
swap(find_LT_right->_val, del_node->_val);
//删除叶节点
//删除之前,要让其父节点的被删除孩子指向空
//这里不是父节点的右孩子原因:左子树只有一个节点
find_LT_right == find_LT_right->_parent->_left ?
find_LT_right->_parent->_left = nullptr
: find_LT_right->_parent->_right = nullptr;
delete find_LT_right;
find_LT_right = nullptr;
}
return true;
}
//查找
Node* find(const V& val)
{
Comp comp;
Node* cur = _root;
while(cur)
{
if(comp(val, cur->_val))
{
cur = cur->_left;
}
else if(comp(cur->_val, val))
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
//中序遍历 -》 有序
void inorder()
{
_inorder(_root);
cout << endl;
}
//节点个数
size_t size()
{
return _size(_root);
}
//求高度
size_t height()
{
return _height(_root);
}
private:
Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
void _inorder(Node* root)
{
if(root == nullptr)
return;
_inorder(root->_left);
cout << root->_val << " ";
_inorder(root->_right);
}
size_t _size(Node* root)
{
if(root == nullptr)
return 0;
return _size(root->_left) + _size(root->_right) + 1;
}
size_t _height(Node* root)
{
if(root == nullptr)
return 0;
//求左子树的高度
size_t left = _height(root->_left);
//求右子树的高度
size_t right = _height(root->_right);
//返回左右子树最大的高度,作为树的高度
return left > right ? left + 1 : right + 1;
}
};