C++进阶——二叉搜索树BST
其实应该是二叉树内容的进阶版本:
二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:
- map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
- 二叉搜索树的特性了解,有助于更好的理解map和set的特性
- 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
- 有些OJ题使用C语言方式实现比较麻烦
既然有了c++这么好的工具不如就再重新加强一下二叉搜索树(BST)。
二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
而且有一个特有意思的特点就是 中序输出是升序的。
举个例子:
int a [] = {5,3,4,1,7,8,2,6,0,9};
二叉搜索树的功能介绍
无非就是增、删、查、改,但是其中最难得的其实是删除。
1.查找
2.插入(增)
插入的具体过程如下:
一. 树为空,则直接插入
二.树不为空,按二叉搜索树性质查找插入位置,插入新节点
三.二叉树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,
再来处理该结点的删除问题
二叉树的实现
创建节点
template<class T>
struct BSTNode
{
BSTNode(const T& key = T()) //构造函数
: _pLeft(nullptr), _pRight(nullptr), _key(key)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
T _key;
};
该节点需要有构造函数。
template<class T>
class BSTree
{
public:
typedef BSTNode<T> node;
typedef node* Pnode;
/*typedef BSTNode<T>* Pnode;*/
private:
Pnode _root = nullptr;
};
每一个节点都需要一个root(node*类型)去调用。
中序打印
为了方便之后数据的检测,需要做一个输出的小工具,因为二叉树严格遵守中序输出是升序的规律,所以我们就设计一个中序打印。
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Pnode root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_pLeft);
cout << root->_key << ' ';
_InOrder(root->_pRight);
}
由于我们平时提供这个接口时不会往里面输入参数,所以我们需要在封装一个函数帮我们输入参数,因此我们的封装了两个接口函数。
增删查改的实现
public:
bool find(const T& key)
{
if (_root == nullptr)
return false;
Pnode cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_pRight;
}
else if (cur->_key < key)
{
cur=cur->_pLeft;
}
else
{
return true;
}
}
return false;
}
查并不难,因为就是挨个遍历(因为二叉树的性质,左边比根小,右边比根大,这样便利可以减少很多时间的)
如果有就返回true,没有就返回false。
bool insert(const T& key)
{
if (_root == nullptr)
{
_root = new node(key);
return true;
}
Pnode parent = nullptr;
Pnode cur = _root;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_pLeft;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_pRight;
}
else
{
return false;
}
}
cur=new node(key);//单独new结点却不进行连接是不行的 ,以下代码不能屏蔽
if (parent->_key > cur->_key)
{
parent->_pLeft = cur;
}
else
{
parent->_pRight = cur;
}
return true;
}
插入也很简单,也是一个一个查找合适的位置,当已经存在了就返回false,
当找到空的时候就可以插入了。
bool erase(const T& key)
{
if (_root == nullptr) return false;
Pnode cur = _root;
Pnode parent = cur;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_pRight;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_pLeft;
}
else
{
if (cur->_pLeft == nullptr)
{
if (cur == _root)
{
_root = cur->_pRight;
}
else
{
if (parent->_pLeft == cur)
{
parent->_pLeft = cur->_pRight;
}
else if (parent->_pRight == cur)
{
parent->_pRight = cur->_pRight;
}
}
delete cur;
}
else if (cur->_pRight == nullptr)
{
if (cur == _root)
{
_root = cur->_pLeft;
}
else
{
if (parent->_pLeft == cur)
{
parent->_pLeft = cur->_pLeft;
}
else if (parent->_pRight == cur)
{
parent->_pRight = cur->_pLeft;
}
}
delete cur;
}
else//左右两边都不为空
{
Pnode PRightMin = cur;
Pnode RightMin = cur->_pRight;
while (RightMin->_pLeft)
{
PRightMin = RightMin;
RightMin = RightMin->_pLeft;
}
cur->_key = RightMin->_key;
if (PRightMin->_pLeft == RightMin)
{
PRightMin->_pLeft = RightMin->_pRight;
}
else
{
PRightMin->_pRight = RightMin->_pRight;
}
delete RightMin;
}
return true;
}
}
return false;
}
删除就会难一点,因为要考察是否要移动子树。
首先你删除的如果是叶子节点,那直接删除就可以了不用考虑别的。
比如:
但是我要删除一个节点他不是叶子节点怎么办?
就需要考虑到领养机制了。(linux中的进程领养是不是很相似?其实也可以想象成一颗进程树)
单子树
当删除的节点在右边,而且有一颗右子树时:
如下图直接把右子树给父节点的右就可以了。
同理“左左”也是这个原理
左右和右左也是差不多的
因为只要是在父节点的左边就是比父节点小,在父节点的右边就是比父节点大。
双子树
最难的是双子树了该怎么办呢?
我只介绍一种方法(另一种类似),就是找到要删除节点的右树的最小节点(右子树的最左一个节点),然后让他替换掉要删除的节点,最后删除这个右树最小的节点。,但是当这个最小节点有右树怎么办呢?
就是把他的右树,托管给最小节点的父亲。
无右树:
有右树
二叉搜索树的应用
-
**K模型:**K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。 -
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生
活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定
单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
<单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key
查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
template<class K, class V>
struct BSTNode
{
BSTNode(const K& key = K(), const V& value = V())
: _pLeft(nullptr) , _pRight(nullptr), _key(key), _Value(value)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
K _key;
V _value
};
template<class K, class V>
class BSTree
{
typedef BSTNode<K, V> Node;
typedef Node* PNode;
public:
BSTree(): _pRoot(nullptr)
{}
// 同学们自己实现,与二叉树的销毁类似
~BSTree();
// 根据二叉搜索树的性质查找:找到值为data的节点在二叉搜索树中的位置
PNode Find(const K& key);
bool Insert(const K& key, const V& value)
{
// ...
PNode pCur = _pRoot;
PNode pParent = nullptr;
while (pCur)
{
pParent = pCur;
if (key < pCur->_key)
pCur = pCur->_pLeft;
else if (key > pCur->_key)
pCur = pCur->_pRight; // 元素已经在树中存在
else
return false;
}
// ...
return true;
}
bool Erase(const K& key)
{
// ...
return true;
}
private:
PNode _pRoot;
};
二叉搜索树的性能分析
1.插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
2.对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的
深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
但是一个搜索二叉树退化成了一个单树枝树,那他的搜索价值就没有了,就完全成了一个链表,该如何改进呢?
就需要接下来讲的平衡二叉树(avl)和红黑树了。