什么是二叉搜索树?
二叉搜索树,可知道他是一个二叉树,并且肯定是为了有和搜索相关的地方,那他到底有什么不一样的呢?
github源码地址 : https://github.com/duchenlong/Cpp/tree/master/SearchTree/SearchTree
概念
所谓二叉搜索树,也叫二叉排序树。他或者是一颗空树,或者他一定具有以下的几个性质
- 如果他的左子树不为空,那么左子树的所有节点的值都
小于根节点
的值 - 如果他的右子树不为空,那么右子树的所有节点的值都
大于根节点
的值 - 他的左右子树也分别为二叉搜索树
类似于这样的一个结构,可以看出,每一个根节点都比他的左子树的值大,比他右子树的值小。
对于二叉搜索树而言,一开始的时候,他的根节点是一个nullptr
,所以当我们把一个数组中的元素按照不同的顺序进行插入的时候,所得到的二叉搜索树的结构可能会不同。
因为我们插入的第一个节点的值就是树的根节点的值,以后所有的数据都是围绕这个根节点进行插入的。所以二叉搜索树不唯一
这也是一颗二叉搜索树
所以说,对于二叉搜索树的描述,我们可以用这样的一个结构体
//存储树中节点的结构体
template<class T>
struct TreeNode
{
T _key;
TreeNode<T>* _left;
TreeNode<T>* _right;
TreeNode(const T key = 0)
:_key(key), _left(nullptr), _right(nullptr)
{}
};
另外,我们可以很直观的看到,对于这棵二叉搜索树,将他按照中序
的方式进行遍历,那么他的遍历的结果就是一个有序的数组。
//中序遍历接口
void _Inorder(TreeNode* root)
{
if (!root) return;
_Inorder(root->_left);
cout << root->_key << ' ';
_Inorder(root->_right);
}
接下来就是一些增删查的接口了
插入节点
在插入元素之前,我们需要明确,首先二叉搜索树是不能存在相同元素的,其次,我么每次所插入的位置一定是一个nullptr节点
的位置。
所以在插入之前,我们需要先找到这个插入的位置,这时就可以利用二叉搜索树的特性,快速的查找到这个位置,当我们遍历到一个节点
- 如果当前节点的值 比 key 大,说明该数据要插在自己的 左子树
- 如果当前节点的值 比 key 小,说明该数据要插在自己的 右子树
然后直到找到nullptr节点的位置再进行插入操作,或者找到了这个节点(就不需要插入了)。
递归插入
递归插入的时候,我们需要注意,我们所插入的位置一定是一个nullptr节点
,空指针是没有地址的,所以我们递归插入的时候,有两种传参方式
- 参数加上父亲节点的指针,然后将申请的节点和父亲节点连接起来
- 使用C++的引用,这样我们直接给
nullptr节点
申请空间的时候,就可以直接和他的父亲节点连接
//递归插入
bool InsertR(const T key)
{
return _InsertR(root, key);
}
//递归插入接口
bool _InsertR(TreeNode*& root, const T key)
{
//根节点为 nullptr ,所以说待插入的位置就是根节点的位置
if (!root)
{
root = new TreeNode(key);
return true;
}
//如果根节点的值和key相同,就不需要插入
if (root->_key == key) return true;
//当前位置不是插入位置,判断需要递归插入左右子树哪里
bool ret = root->_key > key ? _InsertR(root->_left, key) : _InsertR(root->_right, key);
return ret;
}
非递归
//迭代插入
bool Insert(const T key)
{
TreeNode* cur = root;
TreeNode* parent = nullptr;
//找到需要插入的位置,如果这个位置是真,说明还不是待插入的地方
while (cur)
{
//先排除等于的情况
if (cur->_key == key) return true;
parent = cur;
//如果当前节点的值 比 key 大,说明该数据要插在自己的 左子树
//如果当前节点的值 比 key 小,说明该数据要插在自己的 右子树
cur = cur->_key > key ? cur->_left : cur->_right;
}
cur = new TreeNode(key);
//注意当前cur节点为nullptr,他是没有地址的,我们需要把新申请的节点和他的父亲节点手动连接
parent == nullptr ? root = cur : (parent->_key > key ? parent->_left = cur : parent->_right = cur);
return true;
}
查找节点
对于查找,其实也是充分利用了二叉搜索树的特点,左子树都是比自己小的,右子树都是比自己大的
递归查找
//递归查找
TreeNode* FindR(const T key)
{
return _FindR(root, key);
}
//查找调用的接口
TreeNode* _FindR(TreeNode*& root, const T key)
{
//根节点为空,或者根节点为Key,就直接返回root了
if (!root || root->_key == key) return root;
//没找到,就到子树的逻辑中找
TreeNode* ret = root->_key > key ? _FindR(root->_left, key) : _FindR(root->_right, key);
return ret;
}
非递归
//迭代查找
TreeNode* Find(const T key)
{
TreeNode* cur = root;
while (cur)
{
if (cur->_key == key) return cur;
cur = cur->_key > key ? cur->_left : cur->_right;
}
//这里的时候,cur为nullptr ,说明没有找到,返回nullptr
return nullptr;
}
删除节点
删除节点就比较麻烦了,这时候就需要分情况去删除了,有着这样的三种情况:
- 需要删除的节点的左子树是
nullptr
,这个时候我们只需要连接他的右子树和父亲节点就可以了
- 需要删除的节点的右子树是
nullptr
,这个时候我们只需要连接他的左子树和父亲节点
- 需要删除的节点,他的左右子树都不为
nullptr
对于第三种情况,他有着两种删除方式:
- 把删除节点的左子树移动到他右子树的最小的左节点的左子树位置
这样删除,需要面临的一个问题就是,他会使得这棵树的高度变得更高,所以说是不可取的。因为高度变高之后,可能就会失去二叉搜索树原本搜索变快的目的
- 把删除节点右子树的最小的左节点,和删除节点的值进行交换,然后删除这个最小的左节点
递归删除
//递归删除接口
bool _EarseR(TreeNode*& root,const T key)
{
//根节点为空,说明没有key的节点
if (!root) return false;
if (root->_key != key)
return root->_key > key ? _EarseR(root->_left, key) : _EarseR(root->_right, key);
//这里就是root->_key == key 的逻辑
//此时就需要删除 root 节点
TreeNode* del = root;
if (!root->_right) //需要删除的位置的右节点为空,根节点 = 他的左孩子
root = root->_left;
else if (!root->_left) //需要删除位置的左节点为空,根节点 = 他的右孩子
root = root->_right;
else //需要删除节点左右孩子均不为空
{
TreeNode* cur = root->_right;
//找到最后一个左孩子
while (cur->_left)
{
cur = cur->_left;
}
//此时需要把cur放到根节点的位置,然后删除cur节点
root->_key = cur->_key;
//这里可以变成在这个右子树中,删除cur节点
return _EarseR(root->_right, cur->_key);
/*
//这样会让整个树变高的可能
//将该位置的左孩子,插入到第一个右孩子的最后一个左孩子的位置
TreeNode* left = root->_left;
TreeNode* right = root->_right;
//找到第一个右孩子的左孩子中,找到最后的空节点
TreeNode* cur = right;
while (cur)
cur = cur->_left;
//连接左孩子
cur = left;
root = right;
*/
}
delete del;
return true;
}
非递归删除
对于非递归的情况,这个时候我们不能使用引用类型,因为引用会使得原本根节点的位置发生变化。
所以我们需要设置一个变量,这个变量用来记录父亲节点的位置,当这个变量为nullptr时,表示删除的节点是根节点
//迭代删除
bool Earse(const T key)
{
//先查找该节点,同时保留父亲节点的位置
TreeNode* parent = nullptr;
TreeNode* cur = root;
while (cur)
{
if (cur->_key == key) break;
parent = cur;
cur = cur->_key > key ? cur->_left : cur->_right;
}
//如果没有这个节点,就不需要删除
if (!cur) return false;
TreeNode* del = cur;
//此时需要删除cur节点,注意要删除的位置是根节点的情况
if (!cur->_left)
{
if (!parent)
root = cur->_right;
else if (cur == parent->_left) //要删除的节点是左子树
parent->_left = cur->_right;
else if (cur == parent->_right) //要删除的节点是右子树
parent->_right = cur->_right;
}
else if (!cur->_right)
{
if (!parent)
root = cur->_left;
else if (cur == parent->_left)//要删除的节点是左子树
parent->_left = cur->_left;
else if (cur == parent->_right)//要删除的节点是右子树
parent->_right = cur->_left;
}
else
{
TreeNode* minParent = cur;
TreeNode* minLeft = cur->_right;
//找到最后一个左孩子
while (minLeft->_left)
{
minParent = minLeft;
minLeft = minLeft->_left;
}
//此时需要把minLeft放到cur节点的位置,然后删除minLeft节点
cur->_key = minLeft->_key;
//删除minLeft节点,但是防止minLeft的右孩子存在
if (minParent->_right == minLeft) //说明第一个右孩子没有左孩子
minParent->_right = minLeft->_right;
else //删除这个左孩子,他的位置就让 左孩子的右孩子顶替
minParent->_left = minLeft->_right;
del = minLeft;//此时的minLeft变成了要删除的节点
}
delete del;
return true;
}
最后,如果我们一开始建立二叉搜索树的时候,给的数字都是有序的数字,那么就会出现一棵树,他只有左子树,或者只有右子树的情况,这个时候就需要我们去平衡二叉树了。