说明: 前面在C初阶数据结构学习阶段,我对二叉树建立了初步认识,并总结了运用其解决一些问题的方法,但实际上二叉树是一个十分值得深入研究的结构,在有了一定的C++基础的条件下,我将基于C++继续总结二叉树相关的一些更复杂的问题,本篇涉及到的二叉搜索树主要是为后面学习map和set的特性而准备,同时二叉树部分在笔试面试也是考察的重要部分之一,笔记最后也会总结一些复杂的二叉树相关OJ题,欢迎参考指正。【此链接为数据结构阶段本人对初阶二叉树的一些总结,可做参考查阅:http://t.csdn.cn/0gmHK】
二叉搜索树
二叉搜索树的概念:
二叉搜索树又称二叉排序树和二叉查找树,它或者是一棵空树,或是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
得益于二叉搜索树存储的特点,发现二叉搜索树中增删查改的效率会比在其它二叉树中高不少,以存储整数的二叉搜索树为例,以下为其增删查改的具体方式,结合分析及画图的方式,我们需要自己实现这一数据结构来深度理解:
二叉搜索树的增删查改方式:
二叉搜索树的查找:
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。b、最多查找高度次,走到到空,还没找到,这个值不存在。
二叉搜索树的插入:
a.树为空,则直接新增节点,赋值给root指针b. 树不空,按二叉搜索树性质查找插入位置,插入新节点【注意,二叉搜索树默认不允许冗余,因此注意已有值插入为插入失败的特殊处理】
二叉搜索树的删除[重点掌握]:
要全面分析各种情况:
首先查找元素是否在二叉搜索树中,如果不存在,直接返回,否则要删除的结点可能分下面四种情况:a. 要删除的结点无孩子结点【如结点1、4、7、13】b. 要删除的结点只有左孩子结点【如结点14】c. 要删除的结点只有右孩子结点【如结点10】d. 要删除的结点有左、右孩子结点【如结点3、6、8】a和b、c本质上可以当做同一种,因此要处理的一共有三种情况,情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除情况d:在它的右子树中寻找中序下的第一个结点(关键码最小)【或它的左子树中寻找中序下的最后一个结点(关键码最大)】,用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除。
二叉搜索树的实现:
基于以上对二叉搜索树增删查改方式的分析,我们来实现一个简单的二叉搜索树
二叉搜索树结点结构体的定义【结点是构成二叉树的基本结构】:
template <class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)//代表用一个key值来构造一个二叉搜索树结点
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
二叉搜索树类的定义【先简单定义成员函数即可,构造函数等后续再完善】:
template <class K>
class BSTree
{
typedef BSTreeNode<K> Node;//后面要经常用到这个类型,方便起见将其命名为Node
private:
Node* _root=nullptr;//先不写构造函数,故给_root一个缺省值让它直接调默认构造函数
};
二叉搜索树的插入:
如何构造一颗二叉树?其实就是不断给二叉搜索树链入结点的过程,这个过程中插入的第一个结点就是整颗二叉搜索树的根节点_root,需特殊处理。注意,根据前面分析我们知道二叉搜索树插入方式与结点_key值有关,因此将结点按不同顺序插入会使最终二叉搜索树的形状不同。
bool Insert(const K& key)
{
//若_root为空,表明是第一次插入,用该值构造新结点并将其作为根节点
if (_root == nullptr)
{
_root == new Node(key);
}
Node* cur = _root;//定义此指针用于判断
Node* cur = parent;//定义此指针记录cur的父亲指针,用于新结点与其父节点的链接
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if(cur->_key > key)
{
parent = cur;
cur = cur->left;
}
else
{
return false;
}
}
//到此步说明已经找到了新结点的位置
cur = new Node(key);
if (parent->_key < key)//新结点的key值与父亲结点的_key值比较并链接
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//走到这步说明插入成功了,别忘了返回true!!
return true;
}
测试结果:
这样对着监视窗口检查也行,但是检查需要耗费时间去画图验证,基于二叉搜索树的特殊存储结构我们发现按照中序打印二叉搜索树的_key,得到的是一个升序的序列,因此我们直接利用中序遍历思想来实现一个Inorder函数来升序打印此树,每次操作完之后调用就能轻松检查当前函数的实现是否有问题。
搜索二叉树的按序打印:
很多人第一反应会写出如下代码:
void Inorder(Node* root)
{
if (root == nullptr)
return;
Inorder(root->_left);
cout << root->_key << ' ';
Inorder(root->_right);
}
仔细分析,会发现如下问题:
1.假设代码就是如上,当我们调用时就要如下:
BSTree<int> t1;
t1.Inorder(t1._root);_root是私有成员变量,类外是不能调用的,即使是将其权限放大到共有,每次按顺序打印还得传个参数过去,怪别扭的,不符合C++的通常设计。
2.去掉参数也不行,因为此函数内部是递归实现的,没有参数无法实现递归。
3.还有人提出给参数root一个缺省值为_root ,可不可以呢?只能说之前的一些细节没有掌握好,缺省参数是有要求的,缺省值必须是全局变量或常量,_root是全局变量吗?即使把它定义成静态成员变量,成员变量的访问用的是this指针,而this指针是形参,传过来也不是_root
如何解决以上问题?
1.提供一个内部成员函数Getroot(),如下实现调用
BSTree<int> t1;
t1.Inorder(t1.Getroot());虽然还是要传个参数过去,但是却解决了内部保护成员变量不被访问的问题,能不能不传参数就解决。
2.不传参数实现的方法就是再套一层,定义一个公有的无参成员函数Inorder(),再类内部定义一个_Inorder(Node* root),Inorder中调用_Inorder(_root),就可以很好的解决问题了,还可以将_Inorder函数权限设为私有,既能提高设计的可用性,又能在一定程度上保证安全性,
实现如下:
public:
void Inorder()
{
_Inorder(_root);
}
private:
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 (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
测试结果如下:
二叉搜索树的删除【重点掌握】
首先我们找到删除的结点,其次要处理其删除后相关结点【其父节点和子节点】的链接关系,由此我们初步需要定义两个结点,cur指向当前查找的结点,parent指向cur的父节点,cur为空,就说明结点找完了,没返回就是没找到,返回false即可。
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)//包含了_root为nullptr和自顶向底没找到两种情况
{
}
return false;
}
接下来就要完善while循环内部,删除结点分两步:查找+删除,查找逻辑我们上面写过了,代码如下:
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else//找到了
{
}
走到else里,说明就是找到了,接下来就要进行删除操作,根据前面的分析我们要考虑到3种情况,代码如下【既有左孩子又有右孩子的情况处理较为复杂,要着重理解】:
//删除
//1.没有左孩子
if (cur->_left == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
}
//2.没有右孩子
else if (cur->_right == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
}
//3.既有左孩子又有右孩子
else
{
// 找右树最小节点替代,也可以是左树最大节点替代
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;
}
delete minRight;
}
既有左孩子又有右孩子情况图解:
综上,二叉搜索树的删除完整代码如下:
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else//找到了
{
//删除
//1.没有左孩子
if (cur->_left == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
}
//2.没有右孩子
else if (cur->_right == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
}
//3.既有左孩子又有右孩子
else
{
// 找右树最小节点替代,也可以是左树最大节点替代
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;
}
delete minRight;
}
return true;
}
}
return false;
}
测试结果如下:
以上二叉搜索树的查找、插入和删除我们都是用循环写的,我们之前在初阶二叉树学习过程中就知道由于其特殊的树形结构,很多二叉树相关的问题都能用递归来解决,这里不考虑递归层数过多导致栈溢出的情况,用递归实现的方法中有值得我们研究的细节,接下来我们用递归来实现。
递归实现二叉搜索树的查找:
public:
bool FindR(const K& key)
{
return _FindR(_root, key);
}
private:
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key == key)
{
return true;
}
else if (root->_key < key)
{
return _FindR(root->_right, key);
}
else
{
return _FindR(root->_left, key);
}
}
和我们上面分析过Inorder的实现一样,我们同样采用在类内部套一层来定义查找函数,原因同上,下面实现插入删除也是同样的操作,不再赘述。此处子问题描述为:每次递归,先判断根是否为空,如果根为空,说明找完了且没找到,若不为空,则判断根节点_key值与目标值的大小关系,若目标值比根值大,则递归到根结点的右子树去找,若目标值比根值小,则递归到根结点的左子树去找,若相等,则是找到了返回true即可。
递归实现二叉搜索树的插入:
public:
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
private:
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key == key)
{
return false;
}
else if (root->_key < key)
{
return _InsertR(root->_right, key);
}
else
{
return _InsertR(root->_left, key);
}
}
测试结果如下:
这里最值得我们理解的就是_Insert()的第一个参数Node*& root使用引用的原因,基于前面查找、排序函数实现的总结我们写出以下代码不难:
public: bool InsertR(const K& key) { return _InsertR(_root, key); } private: bool _InsertR(Node* root, const K& key) { if (root == nullptr) { root = new Node(key); return true; } if (root->_key == key) { return false; } else if (root->_key < key) { return _InsertR(root->_right, key); } else { return _InsertR(root->_left, key); } }
但以下代码存在的问题就是,找到了要插入的位置且能构造节点,却不能和整棵树链接到一起,有人会想说再给_Insert()增加一个父亲结点参数,当然可以,但实际上我们将第一个参数改为引用就能轻松解决问题,我们通过递归展开图来看:
由上述分析可知,得益于引用的特征,使得我们给root赋值时直接完成了与父结点的链接,非常巧妙,重点掌握!!
递归实现二叉搜索树的删除:
public:
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
protected:
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
_EraseR(root->_right, key);
}
else if (root->_key > key)
{
_EraseR(root->_left, key);
}
else
{
Node* del = root;//记录当前要删除的结点,因为后面会更新root的值,就不再是要删除的结点了
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* leftmax = root->_left;
while (leftmax->_right)
{
leftmax = leftmax->_right;
}
swap(root->_key, leftmax->_key);
return _EraseR(root->_left, key);//这一次递归里一定会走上面的if或else if
}
delete del;//在完成链接后不要忘记要删除结点
return true;
}
}
测试结果如下:
递归删除的子问题可以描述为先判断root是否为空,root为空,说明根节点为空或找完了没找到,直接返回false即可,root不为空,则判断root的_key值与key的大小,若key比root的_key大,则递归到root的右树去删除,小则递归到root的左树去删除,若相等则可以进行删除了,删除仍需判断三种情况,待删除结点左为空,则将它的右树直接链接给它的父节点,待删除结点右为空,则将它的左树链接给它的父节点,链接仍然体现在_EraseR()函数的第一个参数是引用,传来的无论是root->_left还是root->_right都能完成直接删除并链接,特殊的还是待删除结点有左右孩子的情况,这里我们让它与其左树最大_key值结点(或右树最小_key值结点)交换_key值,再在其左树中递归删除交换后的key值结点(或右树中递归删除交换后的key值结点),交换到左树最大(或右树最小)处的结点一定满足没有左孩子或没有右孩子的情况,上面的if或else if就能处理。
无参构造函数、拷贝构造函数、赋值重载、析构函数:
//我们之前没写这个无参构造函数,而是给成员变量了一个缺省值让其调用编译器默认生成,
//下面的拷贝构造本质上也是构造函数,只要写出来,编译器就不会再默认生成无参的默认
//构造,此时就要写出来了,否则会报错
BSTree()
:_root(nullptr)
{}
//或用如下方式强制编译器生成无参的构造函数
BSTree() = default;
//编译器默认生成的拷贝构造是浅拷贝,我们要自己实现深拷贝的拷贝构造
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);//内部封一个拷贝函数,其返回值为拷贝好的新数的根节点
}
//按照前序的方式构造每个结点并拷贝每个节点的key值,链接实际上是按照后续的方式进行的
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& operator = (BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
//析构函数
~BSTree()
{
Destory(_root);
}
void Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
二叉搜索树的完整实现【附带测试代码】
template <class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)//代表用一个key值来构造一个二叉搜索树结点
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
template <class K>
class BSTree
{
typedef BSTreeNode<K> Node;//后面要经常用到这个类型,方便起见将其命名为Node
public:
/*BSTree()
:_root(nullptr)
{}*/
BSTree() = default;//强制编译器生成无参的构造函数
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);
}
BSTree& operator = (BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destory(_root);
}
bool Insert(const K& key)
{
//若_root为空,表明是第一次插入,用该值构造新结点并将其作为根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;//定义此指针用于判断
Node* parent=nullptr;//定义此指针记录cur的父亲指针,用于新结点与其父节点的链接
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if(cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//到此步说明已经找到了新结点的位置
cur = new Node(key);
if (parent->_key < key)//新结点的key值与父亲结点的_key值比较并链接
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//走到这步说明插入成功了,别忘了返回true!!
return true;
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else//找到了
{
//删除
//1.没有左孩子
if (cur->_left == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
}
//2.没有右孩子
else if (cur->_right == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
}
//3.既有左孩子又有右孩子
else
{
// 找右树最小节点替代,也可以是左树最大节点替代
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;
}
delete minRight;
}
return true;
}
}
return false;
}
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return true;
}
}
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);
}
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 _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_key << " ";
_Inorder(root->_right);
}
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key == key)
{
return false;
}
else if (root->_key < key)
{
return _InsertR(root->_right, key);
}
else
{
return _InsertR(root->_left, key);
}
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key == key)
{
return true;
}
else if (root->_key < key)
{
return _FindR(root->_right, key);
}
else
{
return _FindR(root->_left, key);
}
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
_EraseR(root->_right, key);
}
else if (root->_key > key)
{
_EraseR(root->_left, key);
}
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* leftmax = root->_left;
while (leftmax->_right)
{
leftmax = leftmax->_right;
}
swap(root->_key, leftmax->_key);
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
void Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
private:
Node* _root=nullptr;
};
测试代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include"BSTree.h"
void BSTreeTest1()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
int b[] = { 18, 23, 31, 40, 56, 74, 87, 94, 10 };
BSTree<int> t1;
for (auto e : a)
{
t1.InsertR(e);
}
t1.Inorder();
BSTree<int> t2(t1);
t2.Inorder();
cout << "----不同树的赋值----------------------" << endl;
BSTree<int> t3;
for (auto e : b)
{
t3.InsertR(e);
}
t3.Inorder();
t3 = t2;
t3.Inorder();
cout << "------同一棵树自己给自己赋值-----------" << endl;
t1 = t1;
t1.Inorder();
cout << "------连续赋值------------------------" << endl;
t1 = t2 = t3;
t1.Inorder();
t2.Inorder();
t3.Inorder();
//cout << t1.FindR(1) << endl;
//cout << t1.FindR(4) << endl;
//cout << t1.FindR(28) << endl;
/*t1.EraseR(4);
t1.Inorder();
t1.EraseR(14);
t1.Inorder();
t1.EraseR(1);
t1.Inorder();
t1.EraseR(7);
t1.Inorder();
t1.EraseR(13);
t1.Inorder();
t1.EraseR(3);
t1.Inorder();
t1.EraseR(8);
t1.Inorder();*/
}
int main()
{
BSTreeTest1();
return 0;
}
二叉搜索树的应用:
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:比如:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出 现次数就是<word, count>就构成一种键值对。
改造上面实现的二叉搜索树为KV模型:
实现如下:
namespace key_value
{
template <class K,class V>
struct BSTreeNode
{
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _value;
BSTreeNode(const K& key=K(),const V& value=V())//代表用key值和value值来构造一个二叉搜索树结点
:_left(nullptr)
, _right(nullptr)
, _key(key)
,_value(value)
{}
};
template <class K,class V>
class BSTree
{
typedef BSTreeNode<K,V> Node;
public:
/*BSTree()
:_root(nullptr)
{}*/
BSTree() = default;//强制编译器生成无参的构造函数
BSTree(const BSTree<K,V>& t)
{
_root = Copy(t._root);
}
BSTree& operator = (BSTree<K,V> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destory(_root);
}
bool Insert(const K& key,const V& value)
{
//若_root为空,表明是第一次插入,用key和value值构造新结点并将其作为根节点
if (_root == nullptr)
{
_root = new Node(key,value);
return true;
}
Node* cur = _root;//定义此指针用于判断
Node* parent = nullptr;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//到此步说明已经找到了新结点的位置
cur = new Node(key,value);
if (parent->_key < key)//新结点的key值与父亲结点的_key值比较并链接
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//走到这步说明插入成功了,别忘了返回true!!
return true;
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else//找到了
{
//删除
//1.没有左孩子
if (cur->_left == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
}
//2.没有右孩子
else if (cur->_right == nullptr)
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
}
//3.既有左孩子又有右孩子
else
{
// 找右树最小节点替代,也可以是左树最大节点替代
Node* pminRight = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
pminRight = minRight;
minRight = minRight->_left;
}
//注意这里不仅要处理key还要处理value
cur->_key = minRight->_key;
cur->_value = minRight->_value;
if (pminRight->_left == minRight)
{
pminRight->_left = minRight->_right;
}
else
{
pminRight->_right = minRight->_right;
}
delete minRight;
}
return true;
}
}
return false;
}
//注意这里的查找和key模型不太一样,返回值不是布尔值而是结点指针
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
void Inorder()//按序打印一颗二叉搜索树
{
_Inorder(_root);
cout << endl;
}
Node* FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR(const K& key,const V& value)
{
return _InsertR(_root, key,value);
}
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,root->_value);
Newroot->_left = Copy(root->_left);
Newroot->_right = Copy(root->_right);
return Newroot;
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_key<<":"<<root->_value << " ";
_Inorder(root->_right);
}
bool _InsertR(Node*& root, const K& key,const V& value)
{
if (root == nullptr)
{
root = new Node(key,value);
return true;
}
if (root->_key == key)
{
return false;
}
else if (root->_key < key)
{
return _InsertR(root->_right, key,value);
}
else
{
return _InsertR(root->_left, key,value);
}
}
Node* _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return nullptr;
}
if (root->_key == key)
{
return root;
}
else if (root->_key < key)
{
return _FindR(root->_right, key);
}
else
{
return _FindR(root->_left, key);
}
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
_EraseR(root->_right, key);
}
else if (root->_key > key)
{
_EraseR(root->_left, key);
}
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* leftmax = root->_left;
while (leftmax->_right)
{
leftmax = leftmax->_right;
}
swap(root->_key, leftmax->_key);
//处理完key值同样要注意还要处理value值
swap(root->_value, leftmax->_value);
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
void Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
private:
Node* _root = nullptr;
};
}
测试代码如下:
void KVBSTreeTest1()
{
key_value::BSTree<string, string> T1;
T1.InsertR("people", "人");
T1.InsertR("world", "世界");
T1.InsertR("apple", "苹果");
T1.InsertR("happy", "开心");
T1.Inorder();
key_value::BSTree<string, string> T2(T1);
T2.Inorder();
//string str;
//while (cin >> str)
//{
// auto ret = T1.FindR(str);
// if (ret == nullptr)
// {
// cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
// }
// else
// {
// cout << str << "中文翻译:" << ret->_value << endl;
// }
//}
T2.EraseR("people");
T2.Inorder();
//key_value::BSTree<string, string> T3;
//不同树的赋值
//T3 = T2;
//T3.Inorder();
//连续赋值
//T3 = T2 = T1;
//T3.Inorder();
//自己给自己赋值
//T2 = T2;
//T2.Inorder();
}
实例验证:
void TestBSTree4()
{
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" ,"桃子"};
key_value::BSTree<string, int> countTree;
for (const auto& str : arr)
{
// 先查找水果在不在搜索树中
// 1、不在,说明水果第一次出现,则插入<水果, 1>
// 2、在,则查找到的节点中水果对应的次数++
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.Inorder();
}
测试结果如下:
二叉搜索树性能分析:
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。 对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树,如下:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?后续的AVL树和红黑树就由此引出,会在后面章节总结。
二叉树进阶面试题总结:
以下这些题目更适合使用C++完成,难度也更大一些
1. 二叉树创建字符串
题目链接:
分析:本质就是前序遍历,在此基础上对string的基本应用。
Tips1:在将二叉树值val插入字符串之前,先用to_string()将整型转为字符型 ;
Tips2:我们分析题目可知,要用括号将左右子树分别括起来,分三种情况:1.左树为空;2.右树为空;3.左树右树都不为空。这三种情况中,左树不为空,或者左树为空右树不为空时,左树括号不能省略,右树为空括号可以省略,因此可以用上述代码中的条件先判断左后判断右
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
string tree2str(TreeNode* root) {
//一进来如果根为空,直接返回空串即可
if(root==nullptr)
{
return "";
}
//整型转字符型这一步容易遗漏
string s=to_string(root->val);
//这里的条件包含了两种情况:左不为空和左为空右不为空
if(root->left||root->right)//注意:||前面条件不满足才会到||后面去判断
{
s+="(";
s+=tree2str(root->left);
s+=")";
}
//只有右不为空才需要加括号
if(root->right)
{
s+="(";
s+=tree2str(root->right);
s+=")";
}
return s;
}
};
2. 二叉树的分层遍历1【自顶向下】
题目链接:
分析:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
int levelsize=0;//用来记录当前层的数据个数
queue<TreeNode*> q;定义一个存放结点的队列
if(root)//根节点不为空则更新第一层节点个数并将根节点入队
{
levelsize=1;
q.push(root);
}
vector<vector<int>> vv;//定义vv,循环将每一组vector<int>尾插
while(!q.empty())//队列不为空就说明还有结点没入完,循环继续
{
vector<int> v;//定义v,循环将每一层结点的val尾插
while(levelsize--)//levelsize不为0就说明这一层还没出完
{
TreeNode* front=q.front();
q.pop();
v.push_back(front->val);
if(front->left)
{
q.push(front->left);
}
if(front->right)
{
q.push(front->right);
}
}
//每到这一次说明一层走完了,将这层的v尾插到vv
vv.push_back(v);
//同时更新下一层的levelsize
levelsize=q.size();
}
return vv;
}
};
3. 二叉树的分层遍历2【自底向上】
题目链接:
分析:不要想的过于复杂,其实在上一题代码的基础上翻转一下就好,千万别掉进递归的坑
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
int levelsize=0;
queue<TreeNode*> q;
if(root)
{
levelsize=1;
q.push(root);
}
vector<vector<int>> vv;
while(!q.empty())
{
vector<int> v;
while(levelsize--)
{
TreeNode* front=q.front();
q.pop();
v.push_back(front->val);
if(front->left)
{
q.push(front->left);
}
if(front->right)
{
q.push(front->right);
}
}
vv.push_back(v);
levelsize=q.size();
}
//别想复杂了,自顶向上然后翻转一下vv就行
reverse(vv.begin(),vv.end());
return vv;
}
};
4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先【重要】
题目链接:
分析:从我到根节点的所有节点都算是我的祖先,给定两结点p和q,找到这棵树中p、q的最近公共祖先(p、q均存在),当p、q分别在当前结点的左树和右树,当前结点就是p和q的公共祖先,若都在当前结点的左树,则递归到当前结点左树去找公共祖先,反之递归到当前结点的右树去找公共祖先。特殊情况处理,若当前结点是p或q,则当前结点一定是pq的最近公共祖先,直接返回当前结点即可。实现本题的重要一步就是要实现一个判断结点是否在一棵树里的函数,有了这个函数就能轻松控制判断条件,实现如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool IntheTree(TreeNode* root,TreeNode*x)
{
if(root==nullptr)
{
return false;
}
if(root==x)
{
return true;
}
else
{
return IntheTree(root->left,x)||IntheTree(root->right,x);
}
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==nullptr||root->val==p->val||root->val==q->val)
{
return root;
}
bool pinleft=IntheTree(root->left,p);
bool pinright=!pinleft;
bool qinleft=IntheTree(root->left,q);
bool qinright=!qinleft;
//如果p,q分别在根节点的左右两侧,则该根节点为最近公共祖先
if((pinleft&&qinright)||(pinright&&qinleft))
{
return root;
}
if(pinleft&&qinleft)
{
return lowestCommonAncestor(root->left,p,q);
}
if(pinright&&qinright)
{
return lowestCommonAncestor(root->right,p,q);
}
return nullptr;
}
};
以上代码可以通过OJ测试,但发现时间复杂度复杂度较高,达到O(
) ,若要求优化到O(N),有什么办法呢?
1.如果该树是一颗搜索二叉树,时间复杂度就是O(N),可惜不是,pass。
2.构建一个三叉链,将这颗树的结点复制到三叉链,就能转换成链表相交问题来解决,同时时间复杂度达到O(N)。
3.不想自己构造一个三叉链还可以DFS求出p和q的路径分别放到合适的容器中,然后转换成路径相交问题,该思想更合理,我们来实现一下,代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool getpath(TreeNode* root,TreeNode* x,stack<TreeNode*>& path)//path作为输出型参数用引用
{
if(root==nullptr)
{
return false;
}
//前序遍历往下找的时候,先不管它是不是要找的结点,先入栈
path.push(root);
if(root==x)
{
return true;
}
//若左为真,说明在左树,就不去右树找了,直接返回true,说明该结点属于路径结点
if(getpath(root->left,x,path))
return true;
//若左为假,则到右找,找到了也返回true,说明该结点属于路径结点
if(getpath(root->right,x,path))
return true;
//走到这里说明,不在左树也不在右树,则说明当前结点不是路径结点,则将当前结点弹栈然后返回false即可
path.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
stack<TreeNode*> ppath;
stack<TreeNode*> qpath;
getpath(root,p,ppath);
getpath(root,q,qpath);
//题目要求p、q都存在,因此到这里就不用再判断是否为空的情况
//先让size大的栈弹栈到两栈size相等
while(ppath.size()!=qpath.size())
{
if(ppath.size()>qpath.size())
{
ppath.pop();
}
else
{
qpath.pop();
}
}
//再判断栈顶是否相等,不等同时弹栈,遇到相等则该结点就是pq的最近公共祖先
while(ppath.top()!=qpath.top())
{
ppath.pop();
qpath.pop();
}
return ppath.top();
}
};
这种实现是该题最经典的实现方法,需要用到额外的空间,但是时间复杂度只有O(N),需要重点掌握。
5. 二叉树搜索树转换成排序双向链表
题目链接:
分析:如果没有空间限制,我们将这棵树的结点中序拷贝到一个vector中,然后直接按顺序改指针关系是个不错的方法,但是这里如果要求空间复杂度为O(1),即只能在原树上改变指针关系,此时我们就要另寻他法。要改的本质上是每个结点的指向,首先不容置疑的是我们肯定是要去中序遍历搜索二叉树结点,中序遍历的过程中按顺序改变每个节点指针的指向,才能达到题目要求的排序双向链表效果,如何判断当前结点的前驱结点和后继结点呢?前驱结点的判断很简单,就是当前结点的左结点,因为是中序遍历,因此走到当前结点时一定已经走过了其左节点,其前驱结点就是左节点【只需prev=cur->left即可】,但是它的后继结点还没有走,故无法判断,但是当前结点是其前驱结点的后继结点是确定的,因此我们在当前结点可以判断前驱结点的后继结点就是当前结点【只需prev->right=cur,这里要注意prev为空指针的情况】,要注意每次判断完后要在当前栈帧更新prev,最后通过根节点向左找寻找最小结点也就是链表的头结点返回即可。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
void InorderConvert(TreeNode* cur,TreeNode*& prev)
{
if(cur==nullptr)
{
return;
}
InorderConvert(cur->left,prev);
cur->left=prev;
if(prev)
{
prev->right=cur;
}
prev=cur;
InorderConvert(cur->right,prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
TreeNode*prev=nullptr;
InorderConvert(pRootOfTree,prev);
TreeNode* head=pRootOfTree;
while(head&&head->left)
{
head=head->left;
}
return head;
}
};
6. 根据一棵树的前序遍历与中序遍历构造二叉树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder,int& i,int inbegin,int inend)
{
if(inbegin>inend)
{
return nullptr;
}
//先创建根节点
TreeNode* root=new TreeNode(preorder[i]);
//分割左右区间
int rooti=inbegin;
while(rooti<=inend)
{
if(inorder[rooti]==preorder[i])
break;
else
rooti++;
}
i++;
//创建左树
root->left=_buildTree(preorder,inorder,i,inbegin,rooti-1);
//创建右树
root->right=_buildTree(preorder,inorder,i,rooti+1,inend);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
//定义一个i来表示前序遍历数组的下标,设计参数的时候设计为引用
int i=0;
//内部嵌套一个函数,除了两个数组还要传前序的下标用于确定根节点,传中序区间的左右结点,用于确定左右区间边界
return _buildTree(preorder,inorder,i,0,inorder.size()-1);
}
};
7. 根据一棵树的中序遍历与后序遍历构造二叉树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder,int& i,int inbegin,int inend)
{
if(inbegin>inend)
{
return nullptr;
}
//先创建根节点
TreeNode* root=new TreeNode(postorder[i]);
//分割左右区间
int rooti=inbegin;
while(rooti<=inend)
{
if(inorder[rooti]==postorder[i])
break;
else
rooti++;
}
i--;
//创建右树
root->right=_buildTree(inorder,postorder,i,rooti+1,inend);
//创建左树
root->left=_buildTree(inorder,postorder,i,inbegin,rooti-1);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int i=postorder.size()-1;
return _buildTree(inorder,postorder,i,0,inorder.size()-1);
}
};
8. 二叉树的前序遍历,非递归迭代实现
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> v;
TreeNode* cur=root;
while(cur||!s.empty())
{
while(cur)
{
v.push_back(cur->val);
s.push(cur);
cur=cur->left;
}
TreeNode*top=s.top();
s.pop();
cur=top->right;
}
return v;
}
};
9. 二叉树中序遍历 ,非递归迭代实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> v;
TreeNode* cur=root;
while(cur||!s.empty())
{
while(cur)
{
s.push(cur);
cur=cur->left;
}
TreeNode*top=s.top();
s.pop();
v.push_back(top->val);
cur=top->right;
}
return v;
}
};
10. 二叉树的后序遍历 ,非递归迭代实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> v;
TreeNode* cur=root;
TreeNode* prev=nullptr;
while(cur||!s.empty())
{
while(cur)
{
s.push(cur);
cur=cur->left;
}
TreeNode* top=s.top();
//右子树空了或访问完了才能删
if(top->right==nullptr||top->right==prev)
{
v.push_back(top->val);
s.pop();
prev=top;
}
else
{
cur=top->right;
}
}
return v;
}
};