目录
一.二叉搜索树的概念
二叉搜索树本质上还是一颗二叉树,不过多加一些额外的限定条件。若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。若它的右子树不为空,则右子树上所有节点的值都大于根节点的值,并且它的左右子树也分别为二叉搜索树。这样定义所导致的特性就是中序遍历的时候,遍历的结果是升序的。
二.搜索二叉树模拟实现
1.因为是二叉树,我们需要一个节点结构保存每个节点的左右子树和key,vaule值。(这里以key/value模型为例)所以节点结构如下
2. 二叉树的插入
要实现插入首先需要找到正确的位置,我们可以通过比较插入的值和当前节点值的大小,如果当前值大于节点值,说明插入的位置不可能在这个节点的左子树,因为左子树的所有值都小于当前节点的值。反之也是一样。依次向下判断每次排除半颗二叉树。当遇到空节点的时候意味着应该插入这个位置。这个时候需要判断一下插入的位置是左子树还是右子树。最后连接到二叉树上即可
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) //寻找插入位置
{
parent = cur;
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key == key)
{
return false;
}
}
cur = new Node(key, value); //找到位置链接到父节点上
if (parent->_key > key)
{
parent->_left = cur;
}
else if (parent->_key < key)
{
parent->_right = cur;
}
return true;
}
3.二叉树的查找
这个比较简单中序遍历查找可以实现或者用上文的思路也行。这里继续沿用了上文的思路
4.二叉树的删除(方法一:不推荐,但须要理解)
删除的情况稍微复杂一些,首先遍历二叉搜索树找到位置,在删除的时候也包含4种情况。
(1):删除节点没有左右子树:这种情况直接删除就可以了(这一情况其实可以合并到2,3,情况当中)
(2):删除节点只有左子树:比如说这里我们想删除B节点,我们的思路应该是先把A节点接到B的左子树上,再把B释放掉
(3):删除的节点只有右子树:思路和上文一致,看图
(4):删除的节点有左右子树:我们可以采取一种思路叫做替换法。比如说这里我们想删除6,我们需要找到删除36后能顶替6位置的元素。我们可以用6的右子树里的最小值,或者用6左子树里的最大值,也就是4或者7.这里我们以4为例,首先我们交换4和6,然后删掉4就可以了,但这里要注意的一点是交换后删除的节点的位置不见得是叶子节点,我们要先把删除节点的父节点的指针指向删除节点下面的值,对于这里画的这种情况就是指向nullptr
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur) //寻找删除的元素
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key == key)
{
break;
}
else
{
return false;
}
}
//处理左子树为空 (可以兼容左右子树都为空,判断左子树为空后,假若右节点是空,父节点也可以指向nullptr)
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else //两种情况,1.删除节点是父节点的左子树 2.删除节点是父节点的右子树
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else if (cur == parent->_right)
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
//处理右子树为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else //两种情况,1.删除节点是父节点的左子树 2.删除节点是父节点的右子树
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else if (cur == parent->_right)
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else //处理左右子树都不为空
{
Node* minParent = cur;
Node* min = cur->_left;
while (min->_right) //找左子树当中的最右值也就是最大值
{
minParent = min;
min = min->_right;
}
swap(cur->_key, min->_key);
swap(cur->_val, min->_val); //找到节点后交换值
if (minParent->_left == min) //判断删除节点是父节点的左节点还是右节点
{
minParent->_left = min->_left; //指向左节点是因为我们找到的是这课子树的最右值,右节点一定为空。
}
else if (minParent->_right == min)
{
minParent->_right = min->_left;
}
delete min;
}
return true;
}
最后记得根节点的处理
4.二叉树的删除(方法二:较为推荐)
用引用结合递归的方式,具体逻辑没有变化,不过用引用简化了处理指针指向的问题,唯一注意的一点是在交换值之后递归的时候不能直接从根节点开始找。需要手动给出要删除节点所在子树的根节点,不然是找不到的。
bool _EraseR(Node* & root, const K& key)
{
if (root == nullptr)
{
return false;
}
else 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; //引用特性导致不用单独处理指针指向,这里左边的root可以等价为parent->left或者right,
} //这里的值是上一层递归传进来的,通过引用我们可以递归之前的值进行修改
else if (root->_right == nullptr) //右子树为空
{
root = root->_left;
}
else //左右子树都不为空
{
Node* min = root->_left;
while (min->_right)
{
min = min->_right;
}
swap(del->_key, min->_key);
swap(del->_val, min->_val);
return _EraseR(root->_left, key); ;//这里不能直接递归删除key,必须指定为root的左子树,
} //因为,交换后树的结构已经出现问题,假如从根开始找key删除的话,当走到root的位置的时候,此时这个值是小于key的,所以就会进入右子树去寻找,但是要删除的节点在左子树
delete del;
return true;
}
}