在前两篇文章中咱们详细介绍了使用智能指针构建二叉树并进行了层序遍历。html
如今咱们已经掌握了足够的前置知识,能够深刻了解二叉搜索树的查找和删除了。node
本文索引
查找将分为两部分,最值查找和特定值查找。算法
本章中使用的二叉搜索树的结构和上一篇文章中的相同。函数
下面咱们先来看看最值查找。性能
这是最简单的一种查找。测试
根据二叉搜索树的性质,左子树的值都比根节点小,右子树的值都比根节点大,且这一性质对根节点下任意的左子树或右子树都适用。this
根据以上的性质,对于一棵二叉搜索树来讲,最小的值的节点必定在左子树上,且是最左边的一个节点;同理最大值必定是右子树上最右边的那个节点,如图所示:3d
查找的算法也极为简单,只要不停递归搜索左子树/右子树,而后将左边或右边的叶子节点返回,这就是最小值/最大值:指针
NodeType BinaryTreeNode::max() { // 没有右子树时根节点就是最大的 if (!right) { return shared_from_this(); } auto child = right; while (child) { if (child->right) { child = child->right; } else { return child; } } return nullptr; } NodeType BinaryTreeNode::min() { // 没有左子树时根节点就是最小的 if (!left) { return shared_from_this(); } auto child = left; while (child) { if (child->left) { child = child->left; } else { return child; } } return nullptr; }
这里咱们用循环替代了递归,使用递归的实现将会更简洁,读者能够本身留做联系。code
查找特定值的状况较最值要复杂一些,由于须要判断以下几种状况,假设咱们查找的值是value
:
nullptr
。此次咱们决定采用递归实现,基于上述描述使用递归实现更简单,若是有兴趣的话也能够用循环实现,虽然二者在性能上的表现并不会相差太多(由于递归查找的次数只有log2(N)+1次,次数较少没法充分体现循环带来的性能优点):
NodeType BinaryTreeNode::search(int value) { if (value == value_) { return shared_from_this(); } // 继续向下搜索 if (value < value_ && left) { return left->search(value); } else if (value > value_ && right) { return right->search(value); } // 未找到value return nullptr; }
查找算法虽然分了两部分,但和删除节点相比仍是比较简单的。
一般咱们删除一棵树的某个节点时,将其子节点转移给本身的parent便可,然而二叉搜索树须要本身的每一部分都遵照二叉树搜索树的性质,所以对于大部分状况来讲直接将子节点交给parent将会致使二叉搜索树被破坏,因此咱们须要对以下几个状况分类讨论:
只有描述会比较抽象,所以每种状况咱们来看图:
状况a:
红色虚线的部分即为待删除节点,这是直接删除便可。
状况b:
如图所示,当只存在一边的子树时,直接删除节点,将子节点交给parent便可。
状况c较为复杂,咱们举例选择右子树最小值的状况,另外一种状况是类似的:
图中黄色虚线部分就是“待删除节点”,加引号是由于咱们并不真正删除它,而是先要把它的值和右子树的最小值也就是红色虚线部分交换:
交换后咱们删除右子树的最小值节点,这是它知足状况a,所以直接被删除,删除后的树还是一棵二叉搜索树:
这里解释下为何须要交换,首先交换是把状况c尽可能往状况a或b转化简化了问题,同时保证了二叉搜索树的性质;其次若是不进行交换,则须要大量移动节点,性能较差且实现极为复杂,所以咱们才会选择交换节点值的作法。
咱们的代码也会根据上述状况进行分类讨论,此次咱们使用递归实现来简化代码,一样读者若是有兴趣能够研究下循环版本:
// 公开的接口,方便用户调用,具体实如今私有方法remove_node中 void BinaryTreeNode::remove(int value) { auto node = search(value); if (!node) { return; } node->remove_node(); } // 删除节点的具体实现 void BinaryTreeNode::remove_node() { // parent是weak_ptr,须要检查是否可访问 auto p{parent.lock()}; if (!p) { return; } // 状况a,这时判断节点在parent的左侧仍是右侧 // 随后对正确的parent子节点赋值nullptr,当前节点会在函数返回后自动被释放 if (!left && !right) { if (value_ > p->value_) { p->right = nullptr; } else { p->left = nullptr; } return; } // 状况c,选择和右子树最小值交换 if (left && right) { auto target = right->min(); target->remove_node(); // 这里和图解有一点小小的不一样 // 删除target前改变了value_,会致使target被删除时没法正确确认本身是在parent的左侧仍是右侧 // 因此只能在target删除结束后再将值赋值给当前节点 value_ = target->value_; return; } // 下面是状况b的两种可能的形式 // 只存在左子树 if (left) { if (value_ > p->value_) { p->right = left; } else { p->left = left; } left->parent = p; return; } // 只存在右子树 if (right) { if (value_ > p->value_) { p->right = right; } else { p->left = right; } right->parent = p; return; } }
进行分类讨论后代码实现起来也就没有那么复杂了。
如今该测试上面的代码了:
int main() { auto root = std::make_shared<BinaryTreeNode>(3); root->insert(1); root->insert(0); root->insert(2); root->insert(5); root->insert(4); root->insert(6); root->insert(7); root->layer_print(); std::cout << "max: " << root->max()->value_ << std::endl; std::cout << "min: " << root->min()->value_ << std::endl; root->remove(1); // 删除后是否仍是二叉搜索树使用中序遍历便可得知 std::cout << "after remove 1\n"; root->ldr(); root->insert(1); root->remove(5); std::cout << "after remove 5\n"; root->ldr(); }
结果:
如图,二叉搜索树的中序遍历结果是一个有序的序列,两次元素的删除后中序遍历的结果都为有序序列,算法是正确的。