文章目录
前言
在正式介绍今天的二叉搜索树之前,我们要先复习先前阶段的有关于二叉树的知识。首先我们要知道。首先,首先,我们通常定义的二叉树都是链式结构。
//使用c语言定义二叉树的节点
typedef int BinaryTreeValType;
typedef struct BinaryTreeNode
{
BinaryTreeValType val;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BinaryTreeNode;
在多数的oj题中,二叉树也大多是以上面这样的链式结构来定义的。回忆起二叉树的链式结构定义对于理解的正文内容是很有必要的!
正文
前言回顾完成,接下来就是我们正文的内容了。今天我们要来讲的是二叉搜索树。二叉搜索树是stl官方库里面的set和map容器使用的数据结构。不过这两个容器使用的二叉搜索树是相对比较复杂的红黑树。不过本质上还是二叉搜索树。 因为set和map容器常常是面试中会被考察到的知识点。所以我们就正式进入二叉搜索树的学习,简单了解二叉搜索树。
二叉搜索树
1.概念
二叉搜索树,顾名思义就是一棵用来搜索的二叉树,而我们通常认为空树也是一棵二叉搜索树 而如果一棵非空的二叉搜索树满足那它必须要满足如下的几个条件
1.对于任意一个节点,左子树都比它小
2.对于任意一个节点,右子树都比它大
满足这样结构的树我们就可以称之为二叉搜索树。而在理想状态下,二叉搜索树用来搜索的效率可以达到O(logn)是特别好的情况。 而在极端情况下,形成单边树的查找效率就变成了O(n),所以针对二叉搜索树,大佬们又进一步进行了优化—>avl树和红黑树。 不过这些都不是我们今天的重点,我们今天的重点是二叉搜索树。由于C语言的语法的限制,二叉搜索树我们旋转使用c++语言来描述。
2. k模型的二叉搜索树的增删查
首先,我们今天实现的二叉搜索树的两种模型是基于stl官方库中的两个容器:
set和map 而K模型的二叉搜索树对应的就是set容器
首先我们可以快速搭建一个结构出来:
//在二叉搜索树里面,模板参数喜欢命名成K,K有--->key关键字的意思
template<typename K>
struct BinaryTreeNode
{
BinaryTreeNode<K>* _left;
BinaryTreeNode<K>* _right;
K _key;//实际应该使用const k,但是由于删除的需要,所以这里暂且是K
BinaryTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
//有了节点,快速搭建一个二叉树的结构
template<typename K>
class BSTree
{
typedef BinaryTreeNode<K> Node;
public:
//提供默认构造
BSTree()
:_root(nullptr)
{}
private:
Node* _root;
};
到这里,我们基本已经搭建出了一个骨架。接下来我们就来实现这棵二叉树的增删查功能。好奇的同学可能会问,为什么没有改。因为k模型的二叉搜索树本身是按照key来进行查找的,如果随意修改key的话就会乱套!
2.1 查找节点
首先查找节点是非常容易的:直接上代码,不多解释:
//查找函数
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
//当前的key小于待查找的key,去当前节点的右子树查找
if (cur->_key < key)
{
cur = cur->_right;
}
//当前的key大于待查找的key,去当前节点的左子树查找
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
2.2. 增加节点
增加节点和查找节点的思想类似,也是比较简单。不多做解释,都在代码里。
//插入
bool insert(const K& key)
{
//空树特殊处理
if (!_root)
{
_root = new Node(key);
return true;
}
//查找对应的插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
parent = cur;
//走右子树
if (cur->_key < key)
{
cur = cur->_right;
}
//走左子树
else if (cur->_key > key)
{
cur = cur->_left;
}
//已经存在,防止重复插入
else
{
return false;
}
}
//不存在可以插入
Node* newnode = new Node(key);
//插入到左子树
if (parent->_key > key)
{
parent->_left = newnode;
return true;
}
else
parent->_right = newnode;
return true;
}
2.3.删除节点(较难)
接下来,相对比较麻烦的就是二叉搜索树的删除了。因为对应的情况比较复杂。
首先给定一棵二叉搜索树如下图:
情况1:待删除的节点没有左孩子。比如图中的节点8
情况2:待删除的节点没有右孩子:
由于本张图片不好演示,所以寻找了1张新的搜索二叉树来演示
而上面两种情况有一种特例:如果根节点恰好就是只有一个孩子的节点,此时parent为空,所以对于根节点需要特殊处理。
情况3:待删除的节点拥有左右孩子
而对于删除叶子节点的情况,可以归类到只有左孩子和只有右孩子中的任意一种处理。所以完成的删除代码如下:
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 (cur == _root)
{
_root = cur->_right;
}
//这里处理了parent还是nullptr,空指针解引用!
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
}
}
//右子树是空树
else if (cur->_right == nullptr)
{
assert(parent);
if (cur == _root)
{
_root = cur->_left;
}
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_right;
}
delete cur;
}
//两个节点
else
{
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
//覆盖
cur->_key = minRight->_key;
if (minParent->_left == minRight)
{
minParent->_left = minRight->_right;
}
else
{
minParent->_right = minRight->_right;
}
delete minRight;
}
return true;
}
}
//删除节点不在这里面
return false;
}
这就是k模型的二叉搜索树的增删查。接下来我们来看一看
<K,V>模型的二叉搜索树。
3.<k,v>模型的二叉搜索树的增删查改
前面我们介绍了一个k模型的二叉搜索树,stl的set容器也是使用k模型的二叉搜索树。而标准库中还提供了一个map的容器,它所使用的是<k,v>模型的二叉搜索树。我们接下来就看一看这个k,v模型是何方神圣。
3.0 如何理解<K,V>
K模型:所谓的k模型就是key模型。对应的业务场景就是
一个小区里面的所有住户的名字存入到二叉搜索树里。对应用户的姓名就是搜索的需要的结果。
<k,v>模型:这个模型就是一个关键字key对应的一个值val,对应的业务场景是小区业主的门牌号和对应的业主具有一 一对应关系。 通过对应的门牌号可以找到对应业主的名字
而在c++里面,使用pair来构建键值对。
template<typename T1,typename T2>
struct pair
{
pair(T1 t1,T2 t2)
:first(t1)
,second(t2)
{}
T1 first;
T2 second;
};
这里我们先不使用pair,我们先把key和val先分开。
和前面一样,我们先快速打一个框架
template<class K,class V>
struct BinaryTreeNode
{
BinaryTreeNode<K, V>* _left;
BinaryTreeNode<K, V>* _right;
K _key;
V _val;
BinaryTreeNode(const K& key,const V& val)
: _left(nullptr)
, _right(nullptr)
, _key(key)
, _val(val)
{}
};
//二叉树的核心结构
template<typename K,typename V>
class BSTree
{
typedef BinaryTreeNode<K, V> Node;
public:
BSTree()
:_root(nullptr)
{}
private:
Node* _root;
};
3.1 使用递归完成二叉搜索树的查
接下来,我们先上手实现最简单的查找,使用递归查找的思想也比较简单。
当前的key>查找的key,往左子树寻找,当前的key<查找的key,往右子树寻找
所以最后写出的代码就是这样
//递归版本查找--->类内用于递归的子函数
Node* _FindR(Node* root,const K& key)
{
//空树找不到
if (!root)
{
return nullptr;
}
//大于就找左子树
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else if (root->_key < key)
{
return _FindR(root->_right, key);
}
else
{
return root;
}
//return true;
}
和前面不一样的地方是,我们这里返回的是节点的指针。
因为节点的指针可以用来修改对应的val值,但是key同样是不可以被修改的!所以我们选择返回节点的指针而不是布尔值。
3.2 使用递归完成二叉搜索树的增
前面的查找是相对比较好理解,并且代码也不难。那么接下来我们来看一看插入节点。
但是这样就会有一个问题:newnode是一个局部变量,我们无法把这个newnode的局部变量连接到整个二叉树的正确位置。换言之,当递归到合适位置的时候,我们不能把newnode和它的父节点链接到一起!
而解决的方案就是在使用引用传参!
bool _InsertR(Node*& root, const K& key, const V& val)
{
if (!root)
{
root = new Node(key, val);
return true;
}
else if (root->_key > key)
{
return _InsertR(root->_left, key, val);
}
else if (root->_key < key)
{
return _InsertR(root->_right, key, val);
}
return false;
}
接下来,结合代码和递归展开图来好好体会一下,引用带来的魅力。
在这里引用起到了画龙点睛的作用!
3.3使用递归完成二叉搜索树的删(难)
接下来,我们就开始使用递归进行删除二叉树的节点,和非递归的方式一样也是分成三种情况
bool _EraseR(Node*& root,const K& key)
{
if (!root)
{
return false;
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else if (root->_key < key)
{
return _EraseR(root->_right, key);
}
//找到了要删除
else
{
Node* del = root;
//情况1:只有右子树
if (root->_left == nullptr)
{
root = root->_right;
}
//情况2:只有左子树
else if (root->_right == nullptr)
{
root = root->_left;
}
//情况3:左右都有,替代
else
{
Node* minRight = root->_right;
while (minRight->_left)
{
minRight = minRight->_left;
}
//交换
swap(root->_key, minRight->_key);
swap(root->_val, minRight->_val);
return _EraseR(root->_right, key);
}
delete del;
return true;
}
实际上,递归起到的作用仅仅只是寻找。而真正精髓的地方就是删除两个节点的处理
结语
二叉搜索树是一个十分重要的结构,但是我们今天只是实现的二叉搜索树还存在一个很致命的问题: 极端情况下,二叉搜索树会退化成单边树,此时二叉树就退化成了单链表或者是接近单链表! 而为了能够让树接近平衡,大佬们又发明了avl树和红黑树。这两棵树相对结构复杂,但都是建立在今天的基础上。如有错误之处,还望大佬之处。希望大家共同进步!