我们在数据结构中,学习了基本的二叉树的性质,完全二叉树的性质,树和森林的转换,还有哈夫曼树。这些都是较为基础的树,而今天我们来学习一种在
存储数据
上很有特点的树 —二叉搜索树/二叉排序树
那么话不多说,马上开始今天的学习
任何一个节点,他的左子树的所有节点都比他小,右子树的所有节点都比他大。
这就是二叉搜索树
而我们如果中序遍历
输出这棵二叉搜索树,我们可以发现
1->3->4->6->7->8->10->13->14
刚好是升序
了解二叉搜索树
二叉搜索树就是一个数据存储特殊的二叉树,结构上没有特殊部分,所以其结构体和普通二叉树并没有区别
//类模板,用于存储不同数据
template<class K>
struct BinarySearchTree
{
BinarySearchTree<K>*_left;//左子树
BinarySearchTree<K>*_right;//右子树
K _key;//节点值
//构造函数
BinarySearchTree(const K&key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
template<class K>
class BSTree
{
typedef BinarySearchTree<K> Node;
private:
Node*_root = nullptr;//根节点
};
接下来我们来插入数据。
因为二叉搜索树的数据存储特性,所以我们当前不允许有节点值相同
,同时因为其性质,新插入的节点需要先找到其应该在的位置
,然后再构建节点
,链接
,就可以了
我们拿上面的二叉搜索树为例子
构建的数组是这样一组数据{ 8, 3, 1, 10, 6, 4, 7, 14, 13 }
代码如下:
bool Insert(const K&key)
{
//头为空时单独处理
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//循环找插入的位置
Node*cur = _root;
//记录父节点,实现链接
Node*parent = nullptr;
while (cur)
{
parent = cur;
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
//相等则返回假
return false;
}
}
//找到了要插入的位置
cur = new Node(key);
//链接
if (key > parent->_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);
}
因为我们编写在类内,实际使用时不会传参,但递归需要传参,所以我们进行一层封装。
查找其实较为的简单,因为二叉搜索树的特性,我们只要一直比较就好,当前节点的值比查找的值大,则往左边走,比查找的值小,则往右边走
。构建的前半部分也是就是查找
代码如下:
//查找
bool Find(const K&key)
{
Node*cur = _root;
//循环查找
while (cur)
{
//比当前值小,则往左走
if (key<cur->_key)
{
cur = cur->_left;
}//大则往右走
else if (key > cur->_key)
{
cur = cur->_right;
}//不然就是相等,相等就是找到了
else
{
return true;
}
}
//循环没返回说明没查到
return false;
}
删除节点的情况较为复杂,读者可以边看边画图理解。
还是这课树,我们可以先大致把删除节点分为三种情况
叶子结点
,比如:1,4,7,13度为1的节点
,比如:10,14度为2的节点
,比如:6,3,8
叶子节点
叶子结点的删除很简单,只要像查找那样,循环找到节点,然后删除即可。
度为1的节点
度为1的节点,首先也是先找到节点,要删除该节点,我们就需要链接其子树,但程序还需要知道是左子树需要链接,还是右子树需要链接;也不知道是要链接在删除节点父节点的左还是右。
拿10为例子,首先我们找到10,发现其左为空,所以我们需要链接他的右,也就是14,同时我们还需要记录父亲节点,也就是8,知道10是8的右节点,所以我们需要将14链接在8的右
度为2的节点
度为2的节点删除比较难想,其实也是使用
替代法
,但是并不是让其左右节点来替代,而是用其左子树的最右节点/右子树的最左节点
,其实也就是数值和删除节点最接近的节点
,因为左子树的最右节点其实是左子树中最大的
,右子树的最左节点其实是右子树中最小的
,所以这两个节点最接近删除节点
比如我们使用右子树的最小节点来替代,我们以删除8为例子
首先,找到节点8,然后找他的右子树的最小节点,也就是10,我们将10赋给8
,也就是覆盖了原先的8
,然后现在就转变成我们要删除原先的10
了,又因为10是右子树的最左节点
,所以其最多只会有右节点
,不会有左节点,所以我们就又将问题转换成删除度为1的节点
,将该节点的父和其右子树链接就行。注意:这里既可能链接在父节点的左,也可能是右
,所以也需要判断。
小结&特殊情况
但是,删除叶子节点
其实可以和删除度为1的节点
有相同的处理,将其空节点当成子节点
链接就好
还会有一个特殊情况
我们在查找的过程需要记录父亲节点,但是如果是这样一棵树,然后我们要删除10,那么父亲节点就是空
,因为没有进入循环,那么此时的删除是会崩溃的。处理方法之一就是,换根
。
我们直接将根换成3
,这样就既保证了二叉搜索树的结构,又成功删除了节点。
具体代码如下
bool Erase(const K&key)
{
//分成两类
//左或者右为空(包括叶子结点)
//左右孩子都有
//首先先找节点
Node*cur = _root;
//记录父亲节点
Node*parent = nullptr;
while (cur)
{
//parent = cur;
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到了
//分两种情况
//左为空
if (cur->_left == nullptr)
{
//还有可能删到根节点的一边为空(有点像歪脖子树)
if (cur == _root)
{
_root = cur->_right;
}
else
{
//要判断父节点链接左还右
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
return true;
} // 右为空
else if (cur->_right == nullptr)
{
//还有可能删到根节点的一边为空(有点像歪脖子树)
if (cur == _root)
{
_root = cur->_left;
}
else
{
//要判断父节点链接左还右
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
return true;
}
else
{
//左右子树都不为空
//找保姆
//左子树的最大节点 or 右子树的最小节点 二者都可以
// 最右节点 最左节点
Node*pMinRight = cur;//右子树的最小节点的父节点
Node*MinRight = cur->_right;//右子树的最小节点
//找右子树的最左节点
while (MinRight->_left)
{
pMinRight = MinRight;
MinRight = MinRight->_left;
}
//直接赋值
cur->_key = MinRight->_key;
//判断父节点要链接左还是右
if (pMinRight->_left == MinRight)
{
pMinRight->_left = MinRight->_right;
}
else
{
pMinRight->_right = MinRight->_right;
}
//删除MinRight,因为完成交换了
delete MinRight;
MinRight = nullptr;
return true;
}
}
}
return false;
}
因为是树结构,所以我们也可以使用递归完成以上的增删查的操作
因为二叉搜索树的特殊结构,所以其实我们不用遍历所有的节点,只要根据比较结果走相应的路径就好
因为是在类内写递归,所以我们同样需要封装一层
代码如下:
//查找
bool FindR(const K&key)
{
return _FindR(_root, key);
}
bool _FindR(Node*root, const K&key)
{
if (_FindR == NULL)
{
return false;
}
//比较当前节点,相等则返回真
if (root->_key == key)
{
return true;
}
//不相等继续往一边走
if (key < root->_key)
{
return _FindR(root->_left, key);
}
else
{
return _FindR(root->_right, key);
}
}
插入的基本思路也一样,但是我们在递归到空时构建新节点。
链接有三种方式:
- 多传一个参数,记录父节点
- 递归到空节点的上一层,比如if(root->_left==NULL) 开始构建节点
- 传引用
前两个方法和循环写法没什么区别,我们展示一下第三种
代码如下:
//插入
bool InsertR(const K&key)
{
return _InsertR(_root, key);
}
//使用引用,当前的递归可以影响上一层
bool _InsertR(Node*&root, const K&key)
{
if (root == NULL)
{
root = new Node(key);
return true;
}
//因为传参是引用,root相当于父节点的左或右
if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else if(key<root->_key)
{
return _InsertR(root->_left, key);
}
else
{
//相同则返回假
return false;
}
}
我们在传参时,参数是指针的引用,这样我们跳转到下一层递归,下一层递归的root就是上一层root的左节点或者右节点。
递归删除的基本思路和删除一样,也是分为2种情况,叶子节点或者度为1的节点,度为2的节点。
删除第一种情况很简单,我们使用引用
,所以直接赋值
就好。
删除第二种情况也可以像循环那样,但是我们还可以做个应用。
先看代码
//删除
bool EraseR(const K&key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node* &root, const K&key)
{
if (root == NULL)
{
//没找到删除的节点
return false;
}
//递归
if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else if (key < root->_key)
{
return _EraseR(root->_left, key);
}
else
{
//找到了
//保存一下要删除的节点
Node*del = root;
if (root->_left == NULL)
{
//左为空,则链接右
root = root->_right;
}
else if (root->_right == NULL)
{
//右为空,则链接左
root = root->_left;
}
else
{
//还是替代法,找左子树的最大或者右子树的最小
//此处举例左子树的最大
Node*LMax = root->_left;
while (LMax->_right)
{
LMax = LMax->_right;
}
//覆盖,然后从删除节点的左子树重新删除
root->_key = LMax->_key;
return _EraseR(root->_left, root->_key);
//递归回来删除节点
delete LMax;
}
return true;
}
}
删除度为2的节点,我们可以将替换的值覆盖后,转为在删除节点的左子树中,删除替换的节点。
拷贝构造
二叉搜索树的深拷贝
其实同STL的容器一样,需要一个节点一个节点的拷贝,我们使用前序构建,后续链接
的方式拷贝。
代码如下:
//拷贝构造--深拷贝
BSTree(const BSTree<K>&t)
{
_root = Copy(t._root);
}
//深拷贝
//前序构建,后续链接
Node* Copy(Node*root)
{
if (root == nullptr)
{
return nullptr;
}
Node*newRoot = new Node(root->_key);
//链接
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
析构
析构我们需要后续遍历销毁节点。
//析构
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
//销毁二叉搜索树
void Destroy(Node*root)
{
if (root == NULL)
{
return;
}
//先删除左右节点,再删除当前节点
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
赋值重载
我们采用现代写法,套用拷贝构造
//赋值重载
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
因为参数是形参,会发生拷贝构造,我们再用swap交换一下,这样就可以获得新的二叉搜索树。
完整代码如下:
#pragma once
//二叉搜索树
//每个左节点都比根节点小,每个右节点都比根节点大
//类模板,用于存储不同数据
template<class K>
struct BinarySearchTree
{
BinarySearchTree<K>*_left;//左子树
BinarySearchTree<K>*_right;//右子树
K _key;//节点值
//构造函数
BinarySearchTree(const K&key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
~BinarySearchTree()
{
_left = nullptr;
_right = nullptr;
}
};
template<class K>
class BSTree
{
typedef BinarySearchTree<K> Node;
public:
//BSTree()=default;//制定强制生成默认构造
BSTree()
:_root(nullptr)
{}
//拷贝构造--深拷贝
BSTree(const BSTree<K>&t)
{
_root = Copy(t._root);
}
//赋值重载
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
//析构
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
//插入
bool Insert(const K&key)
{
//头为空时单独处理
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//循环找插入的位置
Node*cur = _root;
//记录父节点,实现链接
Node*parent = nullptr;
while (cur)
{
parent = cur;
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
//相等则返回假
return false;
}
}
//找到了要插入的位置
cur = new Node(key);
//链接
if (key > parent->_key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
//中序遍历
//因为二叉搜索树的特点,中序打印出来就是升序
//实现封装
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//查找
bool Find(const K&key)
{
Node*cur = _root;
//循环查找
while (cur)
{
//比当前值小,则往左走
if (key < cur->_key)
{
cur = cur->_left;
}//大则往右走
else if (key > cur->_key)
{
cur = cur->_right;
}//不然就是相等,相等就是找到了
else
{
return true;
}
}
//循环没返回说明没查到
return false;
}
//删除
//删除最好画图
//要考虑极端情况
bool Erase(const K&key)
{
//分成两类
//左或者右为空(包括叶子结点)
//左右孩子都有
//首先先找节点
Node*cur = _root;
//记录父亲节点
Node*parent = nullptr;
while (cur)
{
//parent = cur;
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到了
//分两种情况
//左为空
if (cur->_left == nullptr)
{
//还有可能删到根节点的一边为空(有点像歪脖子树)
if (cur == _root)
{
_root = cur->_right;
}
else
{
//要判断父节点链接左还右
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
return true;
} // 右为空
else if (cur->_right == nullptr)
{
//还有可能删到根节点的一边为空(有点像歪脖子树)
if (cur == _root)
{
_root = cur->_left;
}
else
{
//要判断父节点链接左还右
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
return true;
}
else
{
//左右子树都不为空
//找保姆
//左子树的最大节点 or 右子树的最小节点 二者都可以
// 最右节点 最左节点
Node*pMinRight = cur;//右子树的最小节点的父节点
Node*MinRight = cur->_right;//右子树的最小节点
while (MinRight->_left)
{
pMinRight = MinRight;
MinRight = MinRight->_left;
}
//直接赋值
cur->_key = MinRight->_key;
//判断父节点要链接左还是右
if (pMinRight->_left == MinRight)
{
pMinRight->_left = MinRight->_right;
}
else
{
pMinRight->_right = MinRight->_right;
}
//删除MinRight,因为完成交换了
delete MinRight;
MinRight = nullptr;
return true;
}
}
}
return false;
}
//递归写法
//查找
bool FindR(const K&key)
{
return _FindR(_root, key);
}
//插入
bool InsertR(const K&key)
{
return _InsertR(_root, key);
}
//删除
bool EraseR(const K&key)
{
return _EraseR(_root, key);
}
protected:
//深拷贝
//前序构建,后续链接
Node* Copy(Node*root)
{
if (root == nullptr)
{
return nullptr;
}
Node*newRoot = new Node(root->_key);
//链接
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
//销毁二叉搜索树
void Destroy(Node*root)
{
if (root == NULL)
{
return;
}
//先删除左右节点,再删除当前节点
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
//删除
bool _EraseR(Node* &root, const K&key)
{
if (root == NULL)
{
//没找到删除的节点
return false;
}
//递归
if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else if (key < root->_key)
{
return _EraseR(root->_left, key);
}
else
{
//找到了
//保存一下要删除的节点
Node*del = root;
if (root->_left == NULL)
{
//左为空,则链接右
root = root->_right;
}
else if (root->_right == NULL)
{
//右为空,则链接左
root = root->_left;
}
else
{
//还是替代法,找左子树的最大或者右子树的最小
//此处举例左子树的最大
Node*LMax = root->_left;
while (LMax->_right)
{
LMax = LMax->_right;
}
//覆盖,然后从删除节点的左子树重新删除
root->_key = LMax->_key;
return _EraseR(root->_left, root->_key);
//递归回来删除节点
delete LMax;
}
return true;
}
}
//使用引用,当前的递归可以影响上一层
bool _InsertR(Node*&root, const K&key)
{
if (root == NULL)
{
root = new Node(key);
return true;
}
//因为传参是引用,root相当于父节点的左或右
if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else if (key < root->_key)
{
return _InsertR(root->_left, key);
}
else
{
//相同则返回假
return false;
}
}
bool _FindR(Node*root, const K&key)
{
if (_FindR == NULL)
{
return false;
}
if (root->_key == key)
{
return true;
}
if (key < root->_key)
{
return _FindR(root->_left, key);
}
else
{
return _FindR(root->_right, key);
}
}
//因为要递归,所以要单独编写
//注意此处不可以加缺省值_root,因为缺省值需要是常量
void _InOrder(Node*root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node*_root = nullptr;//根节点
};
本篇知识记录较杂,请多谅解。本着记笔记分享的目的,望佬指点。感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。