二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:
- map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构。
- 二叉搜索树的特性了解,有助于更好的理解map和set的特性。
- 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘。
- 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。
因此本节用C++语言对二叉树部分进行收尾总结。
我们之前学的普通的二叉树其实意义不大,因为如果只是用二叉树来存储数据的话,还不如直接用链表或者顺序表等这些顺序结构。
那二叉树搜索树相对来说,就比较有意义了。
那什么是二叉搜索树呢,先来了解一下它的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树(即它的每一棵子树也满足其左子树所有结点的值都小于根结点的值,右子树的所有结点的值大于根结点的值)
为什么又叫二叉排序树呢?
那了解了搜索二叉树的概念,接下来我们就来手撕一个搜索二叉树。
首先我们来定义一下结点和搜索二叉树的结构
我们之前定义一个模板类,一般都喜欢用T(type),但是在这里比较喜欢用K(key)
相信大家很容易都能看懂,那这里我就不详细解释了。
接下来我们来实现一下向搜索二叉树中插入元素的操作。
首先对于搜索二叉树来说,它的插入应该有插入成功和插入失败(因为搜索二叉树一般不允许出现重复元素)两种情况。
我们来分析一下
首先看插入成功的情况:
在搜索二叉树中,要插入一个元素时,如果可以 插入,那么它插入的位置一定是确定的。
举个栗子
还是以这棵树为例,假设我们现在要插入一个12
那要怎么做呢?
其实就是从根结点开始,去找出那个合适的位置,然后把12放进去。
根结点的值是8,12大于8,所以应该去右子树找,8的右子树是10,12依然大于10,那再看10的右子树,是14,此时12小于14,所以就要往14的左子树,14的左子树为13,12小于13,所以再看13的左子树,是空。
所以,12就应该放在13的左子树上。
此时就插入成功了
那失败的情况呢?
比如,我们现在要插入一个13
首先还是根据大小去比较,找合适的位置,但是走到13的位置发现要插入的值和已经存在的值相等,那就直接return false,插入失败。
当然,如果插入的是第一个结点,那就不需要比较了,直接让它成为根结点。
首先第一个插入的结点是比较特殊的,因为第一个要作为根结点:
那怎么判断是不是第一个插入的呢?
,插入第一个的时候,根结点是不是还是空的啊
所以
如果根结点为空,那就证明是第一次插入,就把它作为根结点。
那其它情况呢?
那后续的插入,就需要我们从根结点开始比较大小去找到合适的位置插入了,思路上面已经讲过了,这里就直接写代码了
按照我们上面讲的思路,走到红框这里,就走到key要插入的正确位置了。
那现在问题来了,如何正确的插入key对应的结点并链接到搜索二叉树上?
,这样写会存在两个问题:
第一个
这里的cur是一个函数里面的局部变量,函数调用结束,cur这个指针变量就被销毁了,销毁了不说,目前我们这样写是不是还会存在内存泄漏啊,cur被销毁了,但是它指向的空间还没释放。
那cur指向的空间不是属于这棵二叉树了嘛,不是最终可以随着搜索树的析构释放吗?
,你把key的结点赋给cur,就链接到树上了嘛,并没有
这就是第二个问题:
把结点给cur,并没有链接到树上。
还看这个例子
走到上面红框的地方,cur只是存了13这个结点(假设取名为parent)的左孩子指针(即parent->left
)的一个指针变量,相当于是parent->left
的拷贝,把结点赋给它,并没有真正链接到13的左孩子上。
那怎么解决呢?
,我们在cur不断向左或向右去找到过程中,记录它的父亲结点,最终cur走到正确的位置,把key的结点直接链接到父亲结点上就可以了
我们来修改一下
代码:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->right;
}
else
{
return false;
}
}
//走到这里cur为空,就是key应该插入的位置
cur = new Node(key);
//链接
if (key < parent->_key)
parent->left = cur;
if (key > parent->_key)
parent->right = cur;
return true;
}
实现好了,行不行呢?测试一下:
那我们写个中序遍历
那写好我就可以调了,但会有一个问题:
那大家思考一下这里可不可以给缺省值?
缺省值给一个_root不就行了。
是不行的,因为缺省值必须是常量或者全局变量(但一般不用全局变量)
这个我们在C++的第一篇文章有提到,大家可以去复习。
而且在参数列表其实根本拿不到成员变量_root,因为访问非静态成员要用this指针,而this指针只能在成员函数内部使用,参数列表也不行。
两个解决方法:
提供一个GetRoot的成员函数/方法,传参的时候通过该方法获取_root。
但C++里不喜欢这样
那其实有另外一种比较简便得方法,给InOrder函数在套一层(封装一下)
这样调的时候就不用传参了,当然我就可以把_InOrder
变成私有的了
然后我们来测试一下
接着我们来看一下查找:
那查找其实是比较简单的:
我们要查找某个结点,那就从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找,找到的话返回true
最多查找高度次,走到空,还没找到,则这个值不存在,返回false
写一下代码:
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;
}
测试一下:
那如果要删除二叉搜索树中的某个结点,应该怎么处理呢?
首先查找元素是否在二叉搜索树中,如果不存在,则直接返回false, 否则要删除的结点可能分下面四种情况:
比如我们现在要删除6
6要怎么删,那这里关键在于把6删了他还有一个孩子怎么处理?
,现在这种情况我们要删除的结点6只有一个孩子,而6被删除的话它的父亲结点3就只剩下一个孩子了,而二叉树中一个结点最多可以有两个孩子,所以6被删除之后,我们是不是可以把它的孩子7托管给6的父亲3的右孩子结点啊。
不一定都是托给右,要进行判断,要看被删除结点是父结点的左还是右孩子
比如我们现在要删除14
那这种情况处理方法其实和上一个一样,也是把被删除结点的孩子托管给其父亲结点,那这里也是托给父亲的右孩子。
另外其实第一种情况也可以归到第二种或第三种里面,第一种是两个孩子都为空,那也可以归到左为空或者右为空的情况里面。
比如我们要删除3或8,怎么删?
首先这里我们上面用到的托管给父结点的方法就不管用了,因为每个结点最多管两个孩子,而现在要删除的这个结点就有两个孩子,如果父亲原本就有两个,那这样父亲要管三个就超了。
况且对于8也就是根结点来说,它根本没有父结点。
那这种情况该如何处理呢?
对于这种情况我们使用的方法叫做——替换法删除法/伪删除法:
以删除8为例,大家看,如果把8删了,谁能够坐到8这个位置呢?
那对于8这棵树来说,其实这个替代者可以有两个人选:
- 左子树的最大值
- 右子树的最小值
那对于当前这棵树其实就是7或者10。
其实仔细观察我们会发现,对于一棵搜索二叉树来说
整棵树中最大的结点就是最右边的那个结点,最小的结点就是最左边的那个结点。
那对于子树来说也是这样,我们看到7其实就是左子树的最右边的结点,10就是右子树最左边的结点。
所以这里,要想8,可以选择用7替换也可以用10。
以用7为例,怎么进行替换删除呢?
那总结一下:
虽然上面我们分析了四种情况,但是我们说了第一种即被删除结点没有孩子的情况可以归到左为空或者右为空的情况里面。
所以总共有三种:
- 左为空
- 右为空
这两种都是托管,但注意具体的代码处理是不一样的,因为一个右为空,一个左为空。- 左右都不为空
替换删除/伪删除法
那下面写一下代码吧
首先我们得查找要删除的元素:
那接下来就是实现删除的逻辑了
左为空或者右为空得删除其实比较好处理,托管给父亲结点即可
最后来实现一下比较难搞的替换删除:
那我们上面说了这个替换结点可以是左数得最大结点(最右边的那个结点),也可以是右树得最小结点(最左边)。
那这里我选择右树的最小结点。
这里的替换删除分为两步:
第一步——找到右树最小结点,然后替换要删除的结点
第二步——删除替换结点
那这里其实有一些需要注意的地方:
我们在这里选择的是右树的最小结点,即右子树最左边的结点,那既然是最左边,他一定没有左孩子了,但是,它可能会有右孩子。
比如像这样
所以这里删除替换结点也要用托管的方式去删,那就需要保存一下替换结点的父亲
那这里还有一些需要特别注意的点:
首先我问大家,现在不是需要保存替换结点的父结点吗?这个父结点的初始值可以给nullptr
吗?
如果看上面那个例子是可以的,因为会进入循环更新parent的值。
但是如果是这样的情况呢?
大家看这种情况是不是不会进入while循环啊,所以pminRight
不会更新,那后面托管的时候就会对空指针解引用,所以这里初始值可以赋cur,即要进行删除的那个结点(伪删除)。
Node* pminRight = cur;
另外我们上面的那个例子
就这个,最终托管的时候是链接到父亲的左子树上,但是不要认为这里找到是最左结点就一定链接到左子树上。
我们现在这个例子:
是不是就是链接到pminRight
的右子树上了,所以要去看minRight(即替换结点)在pminRight的那边。
所以补充完毕应该是这样的
最终删除成功,就return true。
但是呢,其实改到现在还有一个问题:
问题出在哪里呢?
我们直接删除根是没什么问题的现在
但是,如果这样
大家看这种其实就是挂了,我们见过很多次了,调试一下
出现了一个空指针异常(这里是在删除8的时候跳出来这个异常的),怎么回事呢?
我们来分析一下:
这次我们先删除了10、14、13,所以在删除8的时候是这样的
那为什么此时再去删除8就出现parent是空指针的情况呢?
,那问题在于
现在要删8,是右为空的情况
因为根结点没有父亲结点。
那我们怎么解决呢?
那对于这种情况我们可以单独处理一下,其实可以直接更改一下根结点,直接让3成为新的根,然后把8删了就行
那然后我们再来测试
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
//key == cur->_key就是找到了,进行删除
//1.左为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
//2.右为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else//左右都不为空
{
//这里选择右树的最小结点(最左边)替换
Node* pminRight = cur;
Node* minRight = cur->_left;
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;
}
delete minRight;
}
return true;
}
}
//找不到直接返回false
return false;
}
那搜索二叉树主要的操作其实就这些,但是呢,我们上面都是用循环实现的,那搜索二叉树这里呢其实也可以用递归去搞,这三个操作的递归实现我们也有必要去学一下。
查找用递归要怎么写呢?
那具体怎么实现呢?
我们来分析一下:
首先来回顾一下递归的思想:
它通常把一个大型复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,每个子问题都可以进一步分解为更小的子问题,直到达到基本情况(终止条件),然后逐层返回结果,最终得到整个问题的解。
所以这里的思路是这样的:
查找嘛,那就从根结点开始,如果大于根结点的值,就转换为去右子树里面查找,如果小于根结点,就转换为去左子树查找,如果等于就直接返回;那对于子树也是这样,一步步转换为子问题。
如果最后都没找到,一直到空,那就返回false。
那我们写一下代码:
这个没什么难度,我就直接上代码了:
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
return false;
if (key < root->_key)
{
return _FindR(root->_left, key);
}
else if (key > root->_key)
{
return _FindR(root->_right, key);
}
else
{
retrun true;
}
}
试一下:
那插入用递归怎么做呢?
那也是类似的思路,从根节点开始,如果要插入的结点大于根,就转换为去右子树插入;如果要插入的结点小于根,就转换为去左子树插入;如果相等,返回flase;如果一直走到空,那就就在该位置插入就行了。
但是有一个问题,我们找到空插入的时候,如何和它的父亲链接起来?
我们可能会想到这样的方法,比如把父亲作为递归的参数进行传递,或者去判断root的子树为空而不是它本身为空。
但是,最好的方法我觉得是这样:
直接用root的引用就可以了。
因为引用的话,走到空,他就是那个位置指针的引用,直接赋给它就链接上了。
还不用像上面循环实现的那样去判断要连接到那边。
那大家思考一下,我们上面循环的方式,可以用引用吗?
不可以的,因为C++中的引用是不能改变指向的,引用一旦引用一个实体之后,就不能再引用其他实体
而这里递归的话,每次递归都相当于创建了一个新的引用,而不是改变上一层的引用的指向。
测试一下:
怎么实现呢?
那其实大致的思路还是一样的,从根结点开始判断,如果要删除的值大于根,转换为去右子树删,小于根,转换为去左子树删,等于,就进行删除,如果走到空,那就是找不到。
所以关键还是在于如何进行删除。
那我们来分析一下:
其实还是我们上面分析的那三种情况:
左为空、右为空或者左右都不为空。
那这里我们用引用,写起来还是比较简便的。
先写一下左为空和右为空的情况,这两个比较好处理
然后看一下比较麻烦的左右都不为空的情况
我们之前非递归版本的实现是,找一个符合条件的结点替换它,然后把替换的结点删除掉
这里也可以用同样的方法,但我觉得比较简便的方法是这样的:
还是先找一个可以替代要删除结点的结点(左子树最大结点或右子树最小),以左子树最大结点替换为例
交换它们两个的值,然后删除左子树里面的8(key)这个结点就行了
测试一下:
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (key < root->_key)
{
return _EraseR(root->_left, key);
}
else if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else
{
//删除
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* maxLeft = root->_left;
while (maxLeft->_right)
{
maxLeft = maxLeft->_right;
}
swap(maxLeft->_key, root->_key);
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
如果我们想在相对搜索二叉树的对象进行拷贝构造可以吗?
是可以的,虽然我们没写,但是拷贝构造属于默认成员函数,编译器会自动生成,不过默认生成的只完成浅拷贝。
现在没有报错的原因是因为我们没写析构,如果有析构就会出问题,因为搜索二叉树涉及资源申请,这样如果是浅拷贝的话,在析构的时候就会对一块空间析构两次,所以就会出问题。
这都是我们之前学过的内容。
那我们可以先来写一下析构。
那析构的话我们这里还是用递归来搞,也可以用循环,但是比较麻烦:
那现在不出意外,有了析构我们的浅拷贝就要出错了
那我们就来写一个深拷贝的拷贝构造,我们还是用递归来实现
那有了拷贝构造我们得实现一下构造函数:
因为现在有了拷贝构造,编译器就不会生成默认的构造函数了,那就需要我们自己写了
另外C++11有一个关键字——default,可以强制编译器生成默认构造,这个我们后面也会讲
那有了默认构造,我们下面给了缺省值,它走初始列表的时候就会用缺省值去初始化。
现在就可以了。
那赋值重载我们也搞一下吧:
跟我们之前玩的一样
BSTree<K>& operator=(const BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
那关于搜索二叉树的实现差不多就到这里了
gitee
#pragma once
template <class K>
struct BSTreeNode
{
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
template <class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//走到这里cur为空,就是key应该插入的位置
cur = new Node(key);
//链接
if (key < parent->_key)
parent->_left = cur;
if (key > parent->_key)
parent->_right = cur;
return true;
}
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* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
//key == cur->_key就是找到了,进行删除
//1.左为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
cur = nullptr;
}
//2.右为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
cur = nullptr;
}
else//左右都不为空
{
//这里选择右树的最小结点(最左边)替换
Node* pminRight = cur;
Node* minRight = cur->_left;
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;
}
delete minRight;
minRight = nullptr;
}
return true;
}
}
//找不到直接返回false
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
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);
}
/*BSTree()
:_root(nullptr)
{}*/
BSTree() = default;
~BSTree()
{
Destory(_root);
}
BSTree(const BSTree<K>& t)
{
_root = copy(t._root);
}
BSTree<K>& operator=(const BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
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 Destory(Node*& root)
{
if (root == nullptr)
return;
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (key < root->_key)
{
return _EraseR(root->_left, key);
}
else if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else
{
//删除
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* maxLeft = root->_left;
while (maxLeft->_right)
{
maxLeft = maxLeft->_right;
}
swap(maxLeft->_key, root->_key);
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
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 (root == nullptr)
return false;
if (key < root->_key)
{
return _FindR(root->_left, key);
}
else if (key > root->_key)
{
return _FindR(root->_right, key);
}
else
{
return true;
}
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
#define _CRT_SECURE_NO_WARNINGS
#include
using namespace std;
#include "BSTRee.h"
void BSTreeTest1()
{
BSTree<int> t1;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto e:a)
{
t1.Insert(e);
}
t1.InOrder();
cout << t1.FindR(10) << endl;
cout << t1.FindR(19) << endl;
}
void BSTreeTest2()
{
BSTree<int> t1;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto e : a)
{
t1.Insert(e);
}
t1.InOrder();
t1.Erase(10);
t1.InOrder();
t1.Erase(14);
t1.InOrder();
t1.Erase(13);
t1.InOrder();
t1.Erase(8);
t1.InOrder();
for (auto e : a)
{
t1.Erase(e);
t1.InOrder();
}
}
void BSTreeTest3()
{
BSTree<int> t1;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto e : a)
{
t1.InsertR(e);
}
t1.InOrder();
for (auto e : a)
{
t1.EraseR(e);
t1.InOrder();
}
}
void BSTreeTest4()
{
BSTree<int> t1;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto e : a)
{
t1.InsertR(e);
}
t1.InOrder();
BSTree<int> t2=t1;
t2.InOrder();
}
int main()
{
BSTreeTest4();
return 0;
}