目录
以下将进行二叉搜索树增删查的递归实现版本,需要先完全理解迭代版本才能理解递归版本的实现。
一. 增加
//插入值,使用引用传参
bool _insert(Node*& root, const K& key)
{
//当nullptr直接插入,因为此时的root是上一个节点的左指针或右指针
if (root == nullptr)
{
root = new Node(key);
return true;
}
//大于key往左边查找
if (root->_Key > key)
return _insert(root->_left, key);
//小于key往右边查找
else if (root->_Key < key)
return _insert(root->_right, key);
//如果和key一样,说明无需插入
else
return false;
}
//插入值
bool insert(const K& key)
{
return _insert(_root, key);
}
和迭代版本差不多,只是多了一个很巧的地方——节点传参使用引用传参,这里使用引用传参就不需要像迭代那样需要一个父节点指针,解释如下图:
当我们想要插入一个值9时:
按照上述代码,我们先和8进行对比,比8要大往右走,注意此时进入第二个栈帧的root是8这个节点右指针的别名,同时这个指针的值也是10,所以我们就走到了10,发现10大于9,那么我们就要往10的左子树走,也就是说,此时会创建第三个栈帧,并且第三个栈帧的root其实是10这个节点的左指针的别名,也就是说第三个栈帧里的root既是nullptr,也是10的左指针,那么,此时new一个节点给root其实也就是把9插入到了10的左子树里,因为root是10的左指针,所以在将root左指针传给第三个栈帧时,第三个栈帧的root本来就和父节点链接好了,其实也就是刚刚说的,第三个栈帧的root是10这个节点左指针的别名,这本来就已经链接好了。
为什么说巧,因为特殊情况已经被包含,无论是空树插入还是链接时无法确认待插入节点是父节点的左子树还是右子树的问题都能得到解决!
二. 删除
//删除值
bool _erase(Node*& root, const K& key)
{
//当树为空或者找到空了说明没有该值
if (root == nullptr)
return false;
//当比key值大,往左边找
if (root->_Key > key)
return _erase(root->_left, key);
//当比key值小,往右边找
else if (root->_Key < key)
return _erase(root->_right, key);
//走到这说明找到了,需要删除该值,还是三种情况
else
{
Node* del = root;
//左子树为空链接右子树
if (root->_left == nullptr)
root = root->_right;
//右子树为空链接左子树
else if (root->_right == nullptr)
root = root->_left;
//左右子树都不为空
else
{
Node* minRight = root->_right;
while (minRight->_left)
{
minRight = minRight->_left;
}
swap(minRight->_Key, root->_Key);
return _erase(root->_right, key);
}
delete del;
return true;
}
}
//删除值
bool erase(const K& key)
{
return _erase(_root, key);
}
和迭代版本一样,需要讨论被删除节点的左子树为空,右子树为空,左右子树都不为空,这里传参数传的是引用,和插入类似。
要删除需要先查找,左右子树查找,找到待删除值,进到else中进行删除,否则返回false。
当我们需要删除的值如果只有左子树或右子树的情况,以下图是待删除的值只有右子树的情况:
这里我们想要删除10,10比8大,程序创建栈帧,到右子树去寻找,第二个栈帧的root是8这节点右指针的引用同时也是10,此时第二个栈帧的root值为10等于10,走到else中进行删除。
先记录下来待删除的10这个节点,因为它的左子树为空所以走到左子树为空的情况里,由于root是8这个节点右指针的引用,而且同时也是10,那么直接让root被赋值为root的右指针,由于root是8这个节点右指针的引用,那么就相当于8的右指针直接指向root的右指针,而root因为也是10,10的右子树根节点是14,所以直接让8的右指针直接指向14,做到了删除效果的同时又链接回树上了。
同理右子树为空的情况也和上述情况是一致的,既解决了删除,又处理了链接问题。
当我们要删除的值如果左子树和右子树都不为空,此时运用递归的性质将问题缩小化,因为在迭代实现时,左右子树都不为空时,采用的是替代法,而替代法其中一种就是去找待删除节点右子树中的最小值,所以我们可以先去右子树找到最小值,然后将被删除的值和右子树中最小值交换,然后再去递归右子树中找到要删除的节点,进行左右子树为空(即左子树或者右子树为空)情况的删除(一定会走到左右子树都为空)
以下下图为例:
当我们想要删除的值为3时,和8比,比8小,创建第二个栈帧去8的左子树里找,第二个栈帧的root是8这个节点左指针的别名,同时是3,3和3相等,即3是我们想要删除的,3的左右子树不为空
进到左右子树都不为空的情况,维护一个minRight去3这个节点的右子树中寻找右子树中的最小值,即是4,然后将3和4进行交换,也就是说,如上图,此时第二个栈帧的root里的值为4,而minRight值为3,然后再进行递归调用自己这个函数,并且传root的右指针传过去,去找key(即3)
此时创建第三个栈帧,并且第三个栈帧的root是值为4右指针的别名,同时也就是6,比较发现3小于6,就会去6的左边寻找,也就是递归调用自己这个函数,并把6的左指针传过去,去找key(即3)
此时创建第四个栈帧,并且第四个栈帧的root是值为6左指针的别名,同时也就是3,3等于3,即进到else中,因为3的左右子树都为空,但是会优先进入if中,所以此时默认的是左子树为空,就会回到上面的当待删除的值是左子树为空的情况。
最后删除3这个节点,返回true
这里同理,特殊情况删除根节点也是被考虑进来了的。当删除根节点时,即删除8时,8等于8会直接进入到else中并且走待删除节点的左右子树都不为空的情况,minRight被8的右指针赋值,即minRight就是10,由于10的左子树为空,minRight不会进入循环,minRight的值和root即8这个节点的值交换,再将10(交换后的)的右指针传给下一个函数,就变成和和上面删除3的情况一样了,也不会存在nullptr访问问题。
三. 查找
//查找值是否存在
bool find(const K& key)
{
return _find(_root, key);
}
//查找值是否存在
bool _find(Node* root, const K& key)
{
if (root == nullptr)
return false;
if(root->_Key > key)
return _find(root->_left, key);
else if(root->_Key < key)
return _find(root->_right, key);
else
return true;
}
和迭代一致的思想,只是用递归实现,没什么好解释的
总结:需要注意每种情况都需有返回值,因为有可能程序就是那种情况。可以通过画递归展开图加以理解。理解使用引用的的意义,下一个栈帧中的root是上一个栈帧节点传过来的左指针(右指针)的别名和指针所指向的值。