目录
前言:
二叉搜索树的实现
二叉搜索树的基本结构
增
查
中序遍历
删
前言:
在最初学习二叉树的时候,就提及到过单独用树来存储数据是既不如链表也不如顺序表的,二叉树的用处可以用来排序,比如堆排序,也可以用来搜索数据,这是二叉树的用处,用来排序可以实现堆,用来搜索数据可以实现二叉搜索树,即今天实现的一种结构。
那么什么是二叉搜索树呢?
即左孩子比根小,右孩子比根大,且所有的子树都满足这个特点,这就是二叉搜索树,那么是如何实现搜索数据的呢?
搜索数据就是判断大小,最多走高度次个语句就可以找到数据了。
那么找数据的时间复杂度是不是O(logn)呢?很显然不是,万一存在只有左子树或者只有右子树有节点的树呢?那样的话时间复杂度就是O(N)了,所以时间复杂度是O(logN ~ N)。
话不多说,现在开始实现。
二叉搜索树的实现
二叉搜索树的基本结构
template <class T>
struct BSTreeNode
{
BSTreeNode<T>* _left;
BSTreeNode<T>* _right;
T _key;
BSTreeNode(const T& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
template <class T>
class BSTree
{
public:
typedef BSTreeNode<T> Node;
private:
Node* _root = nullptr;
};
这是二叉搜索树的基本结构,每个节点都是一个结构体,好奇的人可能会问为什么值不是val而是key?这是因为二叉搜索树有两个模型,一个是key模型,一个是key-value模型,在key模型中,是不能修改数据的,因为一旦修改了数据整个树的结构就很容易被打乱,在key-value模型中,就可以修改数据,比如有一个数据集合,每个节点都有key和value,每存在一个key,value就++,所以key-value模型中能修改数据,但是修改的是value,即值出现的次数,总结就是能修改的数据就是对整棵树的结构没有影响的数据。
增
增的基本逻辑就是,如果比当前位置的值大,就走右子树,如果比当前位置的值小,就走左子树,如果该树是一个空树,那么这个值就充当根节点。当走到空了,我们就应该考虑连接的部分了,连接的时候,我们需要父节点,判断该值和父节点的大小,再使父节点的左右指针指向这个节点,既然需要父节点,我们这个时候就需要存储父节点的位置,每当走下个节点的时候,就存储一下父节点的位置,基本逻辑就这么多:
bool Insert(const T& val)
{
if (_root == nullptr)
{
_root = new Node(val);
return true;
}
Node* root = _root;
Node* parent = nullptr;
//判断部分
while (root)
{
if (val > root->_key)
{
parent = root;
root = root->_right;
}
else if (val < root->_key)
{
parent = root;
root = root->_left;
}
else
{
return false;
}
}
Node* newnode = new Node(val);
//连接部分 开始判断大小关系
if (parent->_key > val)
{
parent->_left = newnode;
}
else
{
parent->_right = newnode;
}
return true;
}
当然,为了方便,我们都写成了成员函数。
这里有个问题就是,如果存在两个相同的数据怎么办?
实际来说二叉搜索树是不允许存在相同的数据的,这样导致了数据冗余,就像字典里面,存在相同的两个单词吗?不会的是吧,所以我们就不考虑多种相同数据的情况,代码里面返回的就是false。
查
查就很简单了,查就是增的部分代码,遍历一遍,比较有没有这个值就行,遍历多简单,小就走左子树,大就走右子树,相等就返回true:
//查
Node* FindKey(const T& val)
{
Node* root = _root;
while (root)
{
if (val > root->_key)
{
root = root->_right;
}
else if (val < root->_left)
{
root = root->_left;
}
else
{
return root;
}
}
return nullptr;
}
中序遍历
数据加上了,也可以查数据了,我们现在想把数据打印出来看一下怎么办呢?这里推荐使用中序遍历,左子树根右子树这样的顺序打印,因为二叉搜索树的特性,这里打印出来就是升序,看着较为顺眼:
//中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
感觉奇怪吧?为什么InOrder的参数不用Node* root呢?因为存在this指针,this指针是在参数的第一个位置,如果我们传参,传的是根,接受到的还是this指针,就冲突了,所以这里有几个办法,第一个是用一个get set函数,这个方法java比较喜欢使用,第二个方法就是设为私有,套一层函数使用吗,私有函数就没有this指针了。
删
到现在是不是都感觉二叉搜索树没啥?那是因为还没有到删除部分。删除部分才是二叉搜索树的核心。
给定一个二叉搜索树,删除可以分为以下几种情况,第一种情况是删除7 和 14,第二种情况是删除3 和 8。
第一种情况是属于可以直接删除的情况。
对于直接删除的情况,我们分为左右指针都为空,左指针为空,右指针为空的三种情况,实际上,我们可以只分为两种情况,第一种是左指针为空,第二种是右指针为空,比如7,删除7就是让6指向7的任意左右指针就可以了,删除14,我们需要让10的右指针指向13,有一个点就是为什么10指向的地方一定是比10大的?因为二叉树的特性,如果是9,就一定不会在10的下面。
我们可以总结以下,删除的时候,先判断是左为空还是右为空,然后判断子节点和父节点的位置,这样好让父节点指向下一个指针,连接的主要根据就是判断子节点和父节点相对位置。
如果两个都为空怎么办?我们已知一个节点不为空,另一个节点为不为空我们都指向它,总归是没错的。这点可以反证。当我们删除的是根节点的时候,只需要让根节点指向的内容是空就可以了,所以无论我们把删除根节点的位置放在左为空还是右为空都没问题。
到这里两个都为空的问题也就顺理成章的解决了,两个都为空,来就直接走左为空的场景,判断相对位置,父节点连接子节点的右节点,连接的是空指针,解决了就。
这部分的代码如下:
Node* parent = nullptr;
Node* cur = _root;
//先找到 找到该节点才能删除
while (cur)
{
if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
//找到了 开始删除
else
{
///第一种情况:左为空 -> 都为空
if (cur->_left == nullptr)
{
//删除根节点的时候
if (cur == _root)
{
cur = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
///第二种情况:右为空
else if (cur->_right == nullptr)
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
}
}
第二种情况是删除3 和 8 的情况,这种就要麻烦一点,删除的话得用替换法,因为我们没有办法之直接删除它,那么是怎么个替换法呢?我们从树里面找一个数据,满足大于该节点的左节点,小于该节点的右节点,就算是替换完成了。
那么从哪里找这种适配的数据呢?当然是从该节点的左右子树去找了,我们找右子树的最小值,或者是左子树的最大值都可以满足,右子树的最小值,即比右节点的值小,但是同时比左节点大,这就满足了,找到了该值之后,我们要做的是交换数据,交换了数据之后,我们应该怎么样删除右子树的最小值的节点呢?有人提议说用递归删除,比如删除3,用4进行替换,我们删除得先找到这个数据吧,关键问题是根本找不到这个数据,因为交换了数据之后树的结构算是被轻微破坏了,所以我们想要删除就让它的父节点指向空就可以了,此时也要判断一下相对位置即可,总体删除代码如下:
bool EraseKey(const T& val)
{
Node* parent = nullptr;
Node* cur = _root;
//先找到 找到该节点才能删除
while (cur)
{
if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
//找到了 开始删除
else
{
///第一种情况:左为空 -> 都为空
if (cur->_left == nullptr)
{
//删除根节点的时候
if (cur == _root)
{
cur = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
}
///第二种情况:右为空
else if (cur->_right == nullptr)
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
///左右都不为空 -> 替换法
else
{
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
//找右子树的最小值
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
swap(rightMin->_key, cur->_key);
if (rightMinParent->_left == rightMin)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
}
return true;
}
}
return false;
}
最后父节点也可以直接指向空的,但是为了代码的美观性,这样写也不是不行。
感谢阅读!