前言
相信大家一定了解过二分查找吧,在一个有序的数组中查找一个值,例如在下图中查找2,只需要每次取中间的元素比较,就可以排除一半不可能的数据,时间复杂度为O(lgN),但在插入删除的效率为O(N)是不理想的。
我们今天要了解的二叉搜索树查找的效率一般为O(lgN),也会有最坏情况O(N)的时候,不过后序有平衡二叉树解决。
在这里给大家推荐一个视频,可以快速了解二叉搜索树的性质,视频中没有配套代码,只有讲解,看完后可以在来看这篇博客代码,两者结合效果更好。
数据结构合集 - 二叉搜索树(二叉排序树)(二叉查找树)_哔哩哔哩_bilibili
二叉搜索树概念
二叉搜索树(Binary Search Tree)顾名思义是一种特殊的二叉树,主要用于查找与去重,与我们常见的二叉树不同,他对于结点的值有特殊的要求,二叉搜索树的定义如下主要有以下3点。
1.二叉搜索树的左边所有非空结点值小于根结点
2.二叉搜索树的右边所有非空结点值大于根结点
3.二叉搜索树的左右子树也是二叉搜索树
我们接下来看几个例子,判断下是否是二叉搜索树。
第一个就不是二叉搜索树,因为5小于10,不应该在10的右子树上面,应该在10的左子树上面。修改如下图后结果就是正确的了。
接下来我们继续看一个例子
这个是二叉搜索树么?结果是否定的。33大于15不应该出现在15的左子树上面,那么我们把33修改在15的右子树上面如下图对么?
这里我们的33大于15,在15的右子树上面,仿佛没有错,但是我们以30为根节点来看,33大于30,却出现在30的左子树上面,这是错误的!我们定义的第一个要求就是1.二叉搜索树的左边所有非空结点值小于根结点,而不是只看左子树第一个结点就可以了!!所以我们还要修改为如下图。
33此时在30的右子树上面,又在41的左子树上面。
我们最后再看一个例子
这个二叉搜索树是正确的,我们可以如何快速的判断呢?
我们知道二叉搜索树满足 左节点值<根结点<右节点值,于是我们如果进行一次中序遍历,那么二叉搜索树的遍历结果一定是有序的!
上面的中序遍历结果为1 2 4 6 7 8 10 13 14,是有序的所以这个二叉树一定为二叉搜索树。
对于中序遍历我们有一种简单的方法求出结果,分为两步,
第一步是描点,在所有结点下面描个点,如下图
第二步是描边,用一条线,将二叉搜索树从根结点开始描一圈,如下图
然后将线上的按照出现的顺序依次写出来1 3 4 6 7 8 10 13 14,这样就可以得到一个二叉树的中序遍历结果了,前序遍历就是在结点左边描点,后序遍历就是在结点右边描点,大家感兴趣可以尝试,在这里就不多赘述了。
二叉搜索树实现
二叉搜索树与二叉树十分相似,只不过二叉搜索树的结点按照特殊的方式排列罢了,我们就可以定义一个二叉树结点,然后封装在一个二叉搜索树类中实现。
于是我们可以模仿二叉树的实现,先构造出如下的代码框架
#pragma once
#include<iostream>
using namespace std;
template<class T>
struct BSTNode
{
BSTNode(T k= T())
:_left(nullptr)
, _right(nullptr)
, _key(k)
{
}
BSTNode* _left;
BSTNode* _right;
T _key;
};
template<class T>
class BSTree
{
typedef BSTNode<T> node;
public:
BSTree(T k)
{
_root = new node(k);
}
BSTree()
:_root(nullptr)
{
}
private:
node* _root;
};
可以重新定义个头文件,将上述代码放在头文件中,用于实现二叉搜索树,这个采用模板的写法,可以提高代码的复用性。
接下来我们就要开始实现一个数据结构的主要函数即功能了。其中最主要的无外乎增删查改,改在普通的二叉搜索树中没有什么具体的意义,与删操作有些重合,在这里就不再写了。我们首先来实现增加功能。
二叉搜索树的增加
我们定义插入函数的返回值时bool类型,如果key已经在二叉搜索树中,就返回false,插入失败,否则就正常插入。(一般二叉搜索树不支持插入相同的元素,当然也有变种的二叉搜索树支持,这里以前者为例)
bool insert(T k)
{
//二叉搜索树没有结点
if (_root == nullptr)
{
_root = new node(k);
return true;
}
//找到合适的位置
node* parent = nullptr;
node* cur = _root;
while (cur)
{
//提前存下父节点
parent = cur;
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
cur = cur->_right;
else if (cur->_key > k)
cur = cur->_left;
//一般的二叉搜索树不允许出现相同的数字
else
return false;
}
//先判断再插入
if (parent->_key > k)
parent->_left = new node(k);
else
parent->_right = new node(k);
return true;
}
在上面的代码中,我们要记住保留当前位置的父节点,这样我们最后插入数据的时候才知道插在哪里,否则只知道是当前结点为空是连接不了结点的。
其次我们最终知道父亲结点是哪个还要再判断值的大小,否则我们不知道是插入父亲结点的左节点还是右节点。
二叉搜索树的中序遍历
二叉搜索树的中序遍历与普通二叉树一样,都是先遍历左子树,左子树遍历完后,遍历当前结点,然后遍历右子树。
void Inorder(node* _root)
{
if (_root == nullptr)
return;
Inorder(_root->_left);
cout << root->_key << " ";
Inorder(_root->_right);
}
但是这样写是有问题的,我们为了安全性将根结点设置为私有的,再类外面访问不了,于是这样写是不可以的,我们可以将他再次用函数封装以下,如下代码。
void Inorder()
{
_Inorder(_root);
}
private:
void _Inorder(node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_key << " ";
_Inorder(root->_right);
}
我们定义一个子函数_Inorder,他是类里面的函数,可以调用_root,我们在Inorder函数中调用void _Inorder(node* root)函数,从而避免了在类外面输入根结点。
当上面的代码都写完时,我们可以写个测试用例看看,有没有错误。
#include"BSTree.h"
int main()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
t.Inorder();
return 0;
}
头文件"BSTree.h"就包含了我们刚才写的代码。我们就可以通过中序遍历检测我们二叉树构建是否正确。
运行结果如下图,中序遍历是有序的,说明插入的过程中也是正确的。
二叉搜索树的查找
二叉搜索树的查找实现与之前insert函数中找插入结点十分的相似。将key与当前结点比较,如果大于当前结点就遍历右子树,小于就遍历左子树,等于就返回当前结点指针,查询不到就返回空指针。
node* find(T k)
{
node* cur = _root;
while (cur)
{
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
cur = cur->_right;
else if (cur->_key > k)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
同样在写完查找函数后,我们可以写个测试用例来看看是否可以正常的运行。如下测试代码
void test1()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
if (t.find(8))
cout << 8 << endl;
if (t.find(13))
cout << 13 << endl;
if (t.find(0))
cout << 0 << endl;
}
运行结果如下图,结果正确,大家也可以测试多组来判断,这里就不过多的赘述了
二叉搜索树的删除
二叉搜索树的删除较为复杂我们可以一个一个情况来分析看。
1.当前为空树,没有结点,不可能删除成功
2.删除结点没有子树,两个指针指向空
如删除下图中的7
此时只需要删除当前结点,并将其父亲节点6的右指针设置为空就可以了。
//找到删除结点与其父亲结点
node* parent = nullptr;
node* cur = _root;
while (cur)
{
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > k)
{
parent = cur;
cur = cur->_left;
}
else
break;
}
//二叉树中没有改结点
if (cur == nullptr)
return false;
//删除结点没有子树的情况
if (cur->_left == nullptr && cur->_right == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
delete _root;
_root = nullptr;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete cur;
return true;
}
3.删除结点有一个子树,另外一个为空
例如我们要删除下图二叉搜索树中的10
10只有右子树,直接将其父亲结点8的右指针指向10的右子树就可以了。修改后结果如下图
再看下面的例子,删除1
此时只需要将1的父亲结点3的左指针指向4就可以了,修改后如下图
删除只有一个孩子的结点相对来说还是比较简单的,我们要找到要删除节点的父亲结点,并判断删除结点是父亲节点的左边还是右边,子节点的左边还是右边存在,最后连接起来就可以了。
//删除结点只有一个子树的情况
if (cur->_left == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
node* tmp = _root->_right;
delete _root;
_root = tmp;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
node* tmp = _root->_left;
delete _root;
_root = tmp;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
return true;
}
4.删除结点有两个子树
如下图删除3结点
此时3结点的左右子树都不为空,我们要删除3结点,此时有许多种方法
4.1先删除,再重新插入
我们可以以3为根结点遍历下3左右子树值,1 6 4 7,最后再依次将这个数组的值插入到二叉搜索树里面,这样也可以实现,删除某一结点,并保持其他的数据不变,但这种方式较为暴力。
4.2先找到特殊值
假如要删除3,我们想要最大利用原来的数据结构,不从头再来重新插入。可以分析下3位置的数有什么特点。
将3删除后,替代他的数一定再他的左右子树中,这个数要大于左子树任意节点值,又要小于右子树任意值。通过观察图像,可以发现只有左子树最右边的值即左子树最大值,和右子树最左边的值即右子树最小值满足要求。
于是我们便可以找到特殊值来替换要删除的结点,最后在删除特殊结点,从而简化操作。
下面以找左子树最大值为例,只要将结点一直向右移动到为空就行,具体代码等最后再想i下解释,如下是核心代码。
while (leftCur->_right)
{
leftCur = leftCur->_right;
}
最后具体的代码如下
bool erase(T k)
{
//无节点情况
if (_root == nullptr)
return false;
//找到删除结点与其父亲结点
node* parent = nullptr;
node* cur = _root;
while (cur)
{
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > k)
{
parent = cur;
cur = cur->_left;
}
else
break;
}
//二叉树中没有改结点
if (cur == nullptr)
return false;
//删除结点没有子树的情况
if (cur->_left == nullptr && cur->_right == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
delete _root;
_root = nullptr;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete cur;
return true;
}
//删除结点只有一个子树的情况
if (cur->_left == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
node* tmp = _root->_right;
delete _root;
_root = tmp;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
node* tmp = _root->_left;
delete _root;
_root = tmp;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
return true;
}
//删除左右子树都存在 找左子树最右边的值
node* leftParent = cur;
node* leftCur = cur->_left;
while (leftCur->_right)
{
leftParent = leftCur;
leftCur = leftCur->_right;
}
//覆盖删除结点
cur->_key = leftCur->_key;
//左子树只有一个特殊情况
if (leftParent == cur)
leftParent->_left = nullptr;
else//删除结点一定没有右子树,但可能有左子树
leftParent->_right = leftCur->_left;
delete leftCur;
return true;
}
同样可以写个测试用例试试
void test2()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
for (int e : a)
{
t.Inorder();
cout << endl;
t.erase(e);
}
}
运行结果如下与预期结果相同且没有报错,基本确定程序的正确性。这里就测试一例,大家可以多测几次。
上述的第二种情况,两个子树都为空可以合并到第三种情况,看成第三种情况下的特殊情况,于是便可以删除下面代码
//删除结点没有子树的情况
if (cur->_left == nullptr && cur->_right == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
delete _root;
_root = nullptr;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete cur;
return true;
}
删除后,运行结果如下图,依旧是正确的。就可以简化代码。
二叉树的销毁
二叉树的内存是用new在堆上开辟出来的,我们必须要些析构函数来主动的释放内存,否则会造成内存泄漏。
我们可以采用递归式的销毁,先释放左子树再释放右子树,最后释放当前结点。
同理析构函数不可以再外部调用参数,我们可以再设置一个子函数。
~BSTree()
{
BSTDestory(_root);
_root = nullptr;
}
private:
void BSTDestory(node* _root)
{
if (_root == nullptr)
return;
BSTDestory(_root->_left);
BSTDestory(_root->_right);
delete _root;
}
写完后,同样可以测试一下,不过这个要通过调试窗口才可以观察
void test3()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
int c = 0;
}
通过调试窗口可以看见,最后的内存都是成功的释放了的。
如果文章有错误还望多多包涵,在评论区指出。看到这,喜欢的点点关注吧!
附录代码
最后头文件的代码如下
#pragma once
#include<iostream>
using namespace std;
template<class T>
struct BSTNode
{
BSTNode(T k= T())
:_left(nullptr)
, _right(nullptr)
, _key(k)
{
}
BSTNode* _left;
BSTNode* _right;
T _key;
};
template<class T>
class BSTree
{
typedef BSTNode<T> node;
public:
BSTree(T k)
{
_root = new node(k);
}
BSTree()
:_root(nullptr)
{
}
bool insert(T k)
{
//二叉搜索树没有结点
if (_root == nullptr)
{
_root = new node(k);
return true;
}
//找到合适的位置
node* parent = nullptr;
node* cur = _root;
while (cur)
{
//提前存下父节点
parent = cur;
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
cur = cur->_right;
else if (cur->_key > k)
cur = cur->_left;
//一般的二叉搜索树不允许出现相同的数字
else
return false;
}
//先判断再插入
if (parent->_key > k)
parent->_left = new node(k);
else
parent->_right = new node(k);
return true;
}
void Inorder()
{
_Inorder(_root);
}
node* find(T k)
{
node* cur = _root;
while (cur)
{
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
cur = cur->_right;
else if (cur->_key > k)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
bool erase(T k)
{
//无节点情况
if (_root == nullptr)
return false;
//找到删除结点与其父亲结点
node* parent = nullptr;
node* cur = _root;
while (cur)
{
//k大于结点值就去右边,小于就去左边
if (cur->_key < k)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > k)
{
parent = cur;
cur = cur->_left;
}
else
break;
}
//二叉树中没有改结点
if (cur == nullptr)
return false;
//删除结点只有一个子树的情况
if (cur->_left == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
node* tmp = _root->_right;
delete _root;
_root = tmp;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//删除头节点特殊情况
if (parent == nullptr)
{
node* tmp = _root->_left;
delete _root;
_root = tmp;
return true;
}
//判断是父亲结点的左子树还是右子树
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
delete cur;
return true;
}
//删除左右子树都存在 找左子树最右边的值
node* leftParent = cur;
node* leftCur = cur->_left;
while (leftCur->_right)
{
leftParent = leftCur;
leftCur = leftCur->_right;
}
//覆盖删除结点
cur->_key = leftCur->_key;
//左子树只有一个特殊情况
if (leftParent == cur)
leftParent->_left = nullptr;
else
leftParent->_right = leftCur->_left;
delete leftCur;
return true;
}
bool empty(void)
{
return _root == nullptr;
}
~BSTree()
{
BSTDestory(_root);
_root = nullptr;
}
private:
node* _root;
void _Inorder(node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_key << " ";
_Inorder(root->_right);
}
void BSTDestory(node* _root)
{
if (_root == nullptr)
return;
BSTDestory(_root->_left);
BSTDestory(_root->_right);
delete _root;
}
};
源文件的代码如下
#include"BSTree.h"
void test1()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
if (t.find(8))
cout << 8 << endl;
if (t.find(13))
cout << 13 << endl;
if (t.find(0))
cout << 0 << endl;
}
void test2()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
for (int e : a)
{
t.Inorder();
cout << endl;
t.erase(e);
}
}
void test3()
{
BSTree<int> t;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (int e : a)
t.insert(e);
int c = 0;
}
int main()
{
test3();
return 0;
}