在对map/multimap/set/multiset进行了简单的了解后,会发现这几个容器有个共同点是: 其底层都是按照二叉搜索树来实现的。那什么是二叉搜索树?其底层是二叉搜索树吗?
1.二叉搜索树(Binary Search Tree)
1.1二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
示例:
int a[]={5,3,4,1,7,8,2,6,0,9}
1.2二叉搜索树操作
2.1查找
①若根节点不为空
如果根节点key==查找的key,返回ture
如果根节点key>查找key,在其左子树查找(重复①)
如果根节点key<查找key,在其右子树查找(重复①)
②重复①,如果找不到,说明该元素不存在,返回false
PNode Find(const T& data)
{
PNode pCur = _pRoot;
//根节点不为空
while (pCur){
//如果根节点key==查找的key,找到
if (data == pCur->_data)
return pCur;
//如果根节点key<查找key,在其右子树查找
else if (data < pCur->_pLeft)
pCur = pCur->_pLeft;
//如果根节点key>查找key,在其左子树查找
else
pCur = pCur->_pRight;
}
return nullptr;
}
通过 BST 查找节点,理想情况下我们需要检查的节点数可以减半。如下图中的 BST 树,包含了 15 个节点。从根节点开始执行查找算法,第一次比较决定我们是移向左子树还是右子树。对于任意一种情况,一旦执行这一步,我们需要访问的节点数就减少了一半,从 15 降到了 7。同样,下一步访问的节点也减少了一半,从 7 降到了 3,以此类推。根据这一特点,查找算法的时间复杂度应该是 O(log2n)。
实际上,对于 BST 查找算法来说,其十分依赖于树中节点的拓扑结构,也就是节点间的布局关系。下图描绘了一个节点插入顺序为 20, 50, 90, 150, 175, 200 的 BST 树。这些节点是按照递升顺序被插入的,结果就是这棵树没有广度(Breadth)可言。也就是说,它的拓扑结构其实就是将节点排布在一条线上,而不是以扇形结构散开,所以查找时间也为 O(n)。
当 BST 树中的节点以扇形结构散开时,对它的插入、删除和查找操作最优的情况下可以达到亚线性的运行时间 O(log2n)。因为当在 BST 中查找一个节点时,每一步比较操作后都会将节点的数量减少一半。尽管如此,如果拓扑结构像上图中的样子时,运行时间就会退减到线性时间 O(n)。因为每一步比较操作后还是需要逐个比较其余的节点。也就是说,在这种情况下,在 BST 中查找节点与在数组(Array)中查找就基本类似了。
因此,BST 算法查找时间依赖于树的拓扑结构。最佳情况是 O(log2n),而最坏情况是 O(n)。
2.2插入
新插入的节点一定是一个新添加的叶子节点。
BST 的插入算法的复杂度与查找算法的复杂度是一样的:最佳情况是 O(log2n),而最坏情况是 O(n)。 因为它们对节点的查找定位策略是相同的。
a.如果树为空,直接插入,然后返回true
b.树不为空,按照二叉搜索树的性质查找插入位置,插入新节点
bool Insert(const T& data)
{
// 如果树为空,直接插入
if (nullptr == _pRoot){
_pRoot = new Node(data);
return true;
}
// 按照二叉搜索树的性质查找data在树中的插入位置
PNode pCur = _pRoot;
// 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
PNode pParent = nullptr;
while (pCur){
pParent = pCur;
if (data < pCur->_data)
pCur = pCur->_pLeft;
else if (data > pCur->_data)
pCur = pCur->_pRight;
else
// 元素已经在树中存在
return false;
}
// 插入元素
pCur = new Node(data);
if (data < pParent->_data)
pParent->_pLeft = pCur;
else
pParent->_pRight = pCur;
return true;
}
2.3删除
先查找要删除的元素是否存在于二叉搜索树中,如果不存在,则返回,否则要删除的元素分四种情况:
a.要删除的节点无孩子节点
b.要删除的节点只有左孩子节点
c.要删除的节点只有右孩子节点
d.要删除的节点有左右孩子节点
实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题
bool Erase(const T& data) {
// 如果树为空,删除失败
if (nullptr == _pRoot)
return false;
// 查找在data在树中的位置
PNode pCur = _pRoot;
PNode pParent = nullptr;
while (pCur) {
if (data == pCur->_data)
break;
else if (data < pCur->_data) {
pParent = pCur;
pCur = pCur->_pLeft;
}
else {
pParent = pCur;
pCur = pCur->_pRight;
}
}
// data不在二叉搜索树中,无法删除
if (nullptr == pCur)
return false;
Node* Del = pCur;
// 当前节点是叶子节点或者只有左节点---可直接删除
if (nullptr == pCur->_pRight) {
//要删除的节点是根节点
if (pParent == nullptr)
_pRoot = _pRoot->_pLeft;
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pLeft;
else
pParent->_pRight = pCur->_pLeft;
}
// 当前节点只有右孩子---可直接删除
else if (nullptr == pCur->_pLeft) {
//要删除的节点是根节点
if (pParent == nullptr)
_pRoot = _pRoot->_pRight;
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pRight;
else
pParent->_pRight = pCur->_pRight;
}
// 当前节点左右孩子都存在,直接删除不好删除,可以在其子树中找一个替代结点,比如:
// 找其左子树中的最大节点,即左子树中最右侧的节点,或者其右子树中最小的节点,即右子树中最左侧的节点
// 替代节点找到后,将替代节点中的值交给待删除节点,转换成删除替代节点
else {
PNode Replace = pCur->_pRight;
PNode Pre = pCur;//替代节点的双亲节点
if (Replace->_pLeft) {
//用节点右子树的最小节点作为替代节点
Pre = Replace;
Replace = Replace->_pLeft;
}
//将替代节点中的值交给待删除节点,转换成删除替代节点
pCur->_data = Replace->_data;
if (Pre->_pLeft == Replace)
Pre->_pLeft = Replace->_pRight;
else
Pre->_pRight = Replace->_pRight;
Del = Replace;
}
delete Del;
return true;
}
1.3性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log2(N)
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2
2.二叉搜索树的模拟实现
#include <iostream>
using namespace std;
template<class T>
struct BSTNode
{
BSTNode(const T& data = T())
: _pLeft(nullptr)
, _pRight(nullptr)
, _data(data)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
T _data;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
typedef Node* PNode;
public:
BSTree()
: _pRoot(nullptr) {
}
~BSTree() {
_Destroy(_pRoot);
}
void _Destroy(PNode _pRoot) {
if (_pRoot == nullptr)
return;
_Destroy(_pRoot->_pLeft);
_Destroy(_pRoot->_pRight);
delete _pRoot;
}
//拷贝构造
BSTree(const BSTree<T>& tree) {
_pRoot = Copy(tree._pRoot);
}
//赋值运算符重载
BSTree<T>& operator =(const BSTree<T>& tree) {
if (this != &tree) {
_Destroy(_pRoot);
_pRoot = Copy(tree._pRoot);
}
return *this;
}
PNode Copy(PNode root) {
if (root == nullptr)
return root;
PNode NewRoot = new Node(root->_data);
NewRoot->_pLeft = Copy(root->_pLeft);
NewRoot->_pRight = Copy(root->_pRight);
return NewRoot;
}
void Print_Tree() {
Inorder(_pRoot);
cout << endl;
}
void Inorder(PNode root) {//中序遍历二叉搜索树 是其是从小到大的顺序
if (root) {
Inorder(root->_pLeft);
cout << root->_data << " ";
Inorder(root->_pRight);
}
}
PNode Find(const T& data) {
PNode pCur = _pRoot;
while (pCur)
{
if (data == pCur->_data)
return pCur;
else if (data < pCur->_data)
pCur = pCur->_pLeft;
else
pCur = pCur->_pRight;
}
return nullptr;
}
bool Insert(const T& data) {
// 如果树为空,直接插入
if (nullptr == _pRoot) {
_pRoot = new Node(data);
return true;
}
// 按照二叉搜索树的性质查找data在树中的插入位置
PNode pCur = _pRoot;
// 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
PNode pParent = nullptr;
while (pCur) {
pParent = pCur;
if (data < pCur->_data)
pCur = pCur->_pLeft;
else if (data > pCur->_data)
pCur = pCur->_pRight;
else
// 元素已经在树中存在
return false;
}
// 插入元素
pCur = new Node(data);
if (data < pParent->_data)
pParent->_pLeft = pCur;
else
pParent->_pRight = pCur;
return true;
}
bool Erase(const T& data) {
// 如果树为空,删除失败
if (nullptr == _pRoot)
return false;
// 查找在data在树中的位置
PNode pCur = _pRoot;
PNode pParent = nullptr;
while (pCur) {
if (data == pCur->_data)
break;
else if (data < pCur->_data) {
pParent = pCur;
pCur = pCur->_pLeft;
}
else {
pParent = pCur;
pCur = pCur->_pRight;
}
}
// data不在二叉搜索树中,无法删除
if (nullptr == pCur)
return false;
Node* Del = pCur;
// 当前节点是叶子节点或者只有左节点---可直接删除
if (nullptr == pCur->_pRight) {
//要删除的节点是根节点
if (pParent == nullptr)
_pRoot = _pRoot->_pLeft;
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pLeft;
else
pParent->_pRight = pCur->_pLeft;
}
// 当前节点只有右孩子---可直接删除
else if (nullptr == pCur->_pLeft) {
if (pParent == nullptr)
_pRoot = _pRoot->_pRight;
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pRight;
else
pParent->_pRight = pCur->_pRight;
}
// 当前节点左右孩子都存在,直接删除不好删除,可以在其子树中找一个替代结点,比如:
// 找其左子树中的最大节点,即左子树中最右侧的节点,或者其右子树中最小的节点,即右子树中最左侧的节点
// 替代节点找到后,将替代节点中的值交给待删除节点,转换成删除替代节点
else {
PNode Replace = pCur->_pRight;
PNode Pre = pCur;//替代节点的双亲节点
if (Replace->_pLeft) {
//用节点右子树的最小节点作为替代节点
Pre = Replace;
Replace = Replace->_pLeft;
}
//将替代节点中的值交给待删除节点,转换成删除替代节点
pCur->_data = Replace->_data;
if (Pre->_pLeft == Replace)
Pre->_pLeft = Replace->_pRight;
else
Pre->_pRight = Replace->_pRight;
Del = Replace;
}
delete Del;
return true;
}
private:
PNode _pRoot;
};
int main() {
BSTree<int> tree1;
tree1.Insert(3);
tree1.Insert(5);
tree1.Insert(6);
tree1.Insert(4);
tree1.Insert(7);
tree1.Insert(8);
tree1.Print_Tree();
BSTNode<int>* add = tree1.Find(5);
if (add == nullptr)
cout << "二叉树中没有这个值" << endl;
else
cout << "在二叉搜索树找到了该值" <<" "<<add->_data << endl;
tree1.Erase(6);
cout << "删除后的搜索二叉树为:" << endl;
tree1.Print_Tree();
BSTree<int> tree2(tree1);
cout << "Tree2:";
tree2.Print_Tree();
return 0;
}
这里选择中序遍历二叉搜索树,是因为中序遍历二叉搜索树,就是其值从小到大的排序。