BST的Search/Insert/Remove工程实践:从教科书到生产环境
1. 为什么BST的Search/Insert/Remove不是“背三段代码”就能搞定的事Binary Search TreeBST——这个在算法课上被反复演示、在面试题里高频出现的数据结构表面看只是“左小右大”的简单规则。但真正把它用在工程场景里比如实现一个带范围查询的日志索引模块、构建一个支持动态增删的配置项树、或者为某个嵌入式设备设计轻量级内存管理器时你会发现Search是基础Insert是入口Remove才是分水岭。它不像链表删除节点那样只需改指针也不像数组插入那样挪动数据就行。BST的Remove操作天然携带结构性破坏风险——一旦处理不当整棵树的搜索性质即BST性质就会崩塌后续所有Search都可能返回错误结果。我最早在写一个实时告警规则引擎时踩过这个坑。当时用C手撸BST管理上千条动态规则Insert和Search跑得飞快但某天运维同事反馈“规则匹配突然失效”排查三天才发现是Remove操作中漏掉了对双子节点节点即左右孩子都非空的后继节点替换逻辑导致树形退化成单链Search时间复杂度从O(log n)恶化到O(n)而监控指标又没覆盖树高变化问题被掩盖了整整两周。这件事让我彻底明白BST的Remove不是语法练习而是对数据结构本质的一次压力测试。关键词“Search Insert Remove”背后实际藏着三个层次的能力要求Search考察你对递归/迭代路径的理解是否扎实Insert考验你对树生长边界的把控是否精准而Remove则直接检验你能否在局部修改中维持全局约束。这三者环环相扣——Insert若不保证BST性质Search必然出错Remove若破坏结构Insert和Search全盘失效。所以本文不讲“怎么写”而是拆解“为什么必须这样写”把教科书里的伪代码还原成真实世界里需要权衡的工程决策比如为什么后继节点要选右子树的最左节点而非左子树的最右节点为什么递归实现比迭代实现更容易遗漏边界条件为什么在多线程环境下这三个操作必须加锁而锁的粒度又不能粗暴地锁整棵树接下来的内容全部基于我在工业级中间件、嵌入式固件、以及高频交易系统中实际落地BST的经验。没有抽象理论推导只有可验证的代码片段、可复现的调试现场、以及那些只在深夜debug时才会浮现的细节真相。2. Search操作从“找到值”到“定位位置”的思维跃迁BST的Search看似最简单——给定目标值从根开始比较小了往左、大了往右相等就返回。但如果你只停留在“返回true/false”或“返回节点指针”层面说明还没真正吃透它的工程价值。真实场景中Search从来不是孤立动作它往往是Insert或Remove的前置步骤其输出必须能无缝衔接到后续操作。这意味着Search函数的设计必须回答一个关键问题我们到底需要“什么”2.1 三种Search接口设计及其适用场景很多初学者直接写一个bool search(int val)这在LeetCode上能AC但在生产环境里会立刻暴露短板。我见过至少三类因Search接口设计不当引发的线上事故事故类型一Insert时重复插入某IoT设备固件用BST管理传感器ID列表每次新设备接入先Search再Insert。但Search只返回boolInsert逻辑写成if (!search(id)) insert(id);。问题在于当Search发现id已存在时它本应返回该节点的父节点指针以便Insert跳过而bool返回值迫使Insert重新遍历一次树找插入位置白白消耗CPU周期。在资源受限的MCU上这直接导致心跳包延迟超标。事故类型二Remove时无法定位父节点某金融风控系统用BST存储实时交易限额Remove操作需调整父节点指针。但Search只返回目标节点地址没有提供父节点信息。开发同学只好在Remove里重写一遍Search逻辑找父节点结果两套遍历路径对同一棵树做不同判断比如浮点数比较精度差异导致父节点指针错位树结构被意外切断。事故类型三范围查询时无法获取边界节点某日志分析平台用BST按时间戳索引日志条目需支持“查询2023-01-01至2023-01-31之间的所有日志”。Search若只返回单个节点就无法高效获取范围起始和结束位置只能暴力遍历整棵树。因此一个工业级BST的Search接口至少应提供三种变体接口签名返回值含义典型使用场景性能代价Node* search(int val)目标节点指针未找到返回nullptr单值查询、存在性校验O(h)h为树高std::pairNode*, Node* searchWithParent(int val){目标节点, 父节点}根节点父节点为nullptrInsert前检查、Remove操作主入口O(h)但需额外维护父指针变量std::pairNode*, Node* searchRangeBoundary(int low, int high){范围左边界节点, 范围右边界节点}用于中序遍历截取时间范围查询、数值区间统计O(h k)k为范围内节点数提示searchWithParent是Insert和Remove的黄金搭档。它避免了二次遍历且父节点信息是执行指针重连的必要条件。不要试图在Remove里“自己找父节点”——那等于把同一个bug写两遍。2.2 迭代实现为何比递归更受生产环境青睐教科书常用递归实现Search代码简洁Node* search_recursive(Node* root, int val) { if (!root || root-val val) return root; if (val root-val) return search_recursive(root-left, val); else return search_recursive(root-right, val); }但我在为车载ECU开发BST时被编译器警告逼着改成了迭代版。原因很现实递归深度不可控栈空间会溢出。假设树高h1000完全不平衡时递归调用栈就要压入1000帧每帧至少8字节返回地址参数光栈空间就超8KB在RAM仅512KB的汽车MCU上这是致命的。迭代版不仅规避栈溢出还带来两个隐藏优势调试友好性GDB单步调试时你能清晰看到每一步的current和parent指针变化而递归调用栈里堆叠着几十层search_recursive根本分不清哪一层对应哪个节点。扩展性强当需要记录搜索路径比如为AVL树计算平衡因子、或在搜索中途注入监控逻辑如统计热点查询路径时迭代循环的while结构天然支持插入钩子代码。以下是经过实战验证的迭代Search实现含父节点追踪// 返回 {目标节点, 父节点}根节点的父节点为nullptr std::pairNode*, Node* search_iterative(Node* root, int val) { Node* current root; Node* parent nullptr; while (current ! nullptr) { if (current-val val) { break; // 找到目标current即所求 } else if (val current-val) { parent current; current current-left; } else { parent current; current current-right; } } return {current, parent}; }注意parent的更新时机只有在向子节点移动时才更新parent。如果current是根节点且值不匹配parent保持为nullptr这恰好符合“根节点无父节点”的语义。这个细节在Remove操作中至关重要——当删除根节点时你需要知道它的父节点是空的从而正确更新树的根指针。2.3 Search中的魔鬼细节相等性判断与浮点数陷阱BST的Search逻辑依赖于val current-val和val current-val的严格比较。这在整数场景下毫无问题但一旦涉及浮点数灾难就开始了。某次为医疗设备开发心率分析模块时我们用BST存储采样时间戳double类型。Search操作始终无法命中已Insert的节点日志显示val current-val永远为false。根源在于浮点数在计算机中是近似表示0.1 0.2 ! 0.3是常识但工程师常忽略BST比较中的累积误差。解决方案不是放弃浮点数而是重构比较逻辑// 浮点数安全的Searchtolerance为预设容差如1e-9 std::pairNode*, Node* search_float_safe(Node* root, double target, double tolerance 1e-9) { Node* current root; Node* parent nullptr; while (current ! nullptr) { double diff target - current-val; if (std::abs(diff) tolerance) { break; // 认为相等 } else if (diff -tolerance) { parent current; current current-left; } else { parent current; current current-right; } } return {current, parent}; }注意容差值tolerance不能随意设。它必须与业务场景的物理意义对齐。例如心率采样时间戳毫秒级精度已足够tolerance1e-31毫秒比1e-9更合理。过大容差会导致误匹配过小则失去浮点校正意义。这个例子揭示了一个底层原则BST的Search行为本质上由其比较函数定义。整数比较是离散的浮点比较是连续的而自定义类型如字符串、结构体的比较函数直接决定了BST的“形状”和“行为”。所以永远不要假设是绝对可靠的要把比较逻辑显式封装为未来扩展留出接口。3. Insert操作如何让树“自然生长”而不失控Insert是BST的“入口”它决定了树的初始形态和后续演化路径。很多人以为Insert就是“找到空位放进去”但真实世界里Insert的每一次执行都在悄悄改变树的平衡性、查询效率甚至影响Remove的复杂度。我曾维护过一个运行5年的BST配置中心最初Insert随机插入树高从7缓慢爬升到23Search平均耗时翻了3倍而监控系统对此毫无感知——因为没人给“树高增长率”设告警阈值。3.1 Insert的两种哲学严格BST vs 容忍重复BST的经典定义要求“左子树所有节点值小于根右子树所有节点值大于根”这意味着不允许重复值。但工程实践中重复值几乎不可避免日志时间戳可能毫秒级重复用户ID在分布式系统中可能因时钟漂移产生碰撞传感器读数在噪声范围内恒定。面对重复值Insert有两种主流策略策略A拒绝插入Strict BST当val current-val时直接返回失败。这是教科书做法保证BST性质绝对纯净。适用于强一致性要求的场景如证书序列号管理、唯一订单ID索引。策略B允许重复BST with Duplicates将重复值插入到左子树或右子树通常约定插入到右子树。这牺牲了严格的BST定义但换来了更高的数据包容性。适用于日志、监控、采样数据等场景。选择哪种策略取决于你的数据语义。我建议用一个枚举明确声明enum class DuplicatePolicy { REJECT, // 遇重复返回错误 INSERT_RIGHT,// 重复值插入右子树 INSERT_LEFT, // 重复值插入左子树少见 COUNT // 不插入仅增加节点计数器需Node结构支持count字段 }; // Insert接口需接收policy参数 bool insert(Node* root, int val, DuplicatePolicy policy DuplicatePolicy::REJECT);实操心得COUNT策略在资源敏感场景极具价值。比如在嵌入式设备中统计某类错误码出现频次无需为每个重复错误码创建新节点只需在原节点上count内存占用直降80%。但要注意这会让Search返回的节点可能包含多个逻辑实体上层业务需适配。3.2 Insert的临界点为什么必须检查空树和根节点Insert操作看似简单但有两个极易被忽略的边界条件它们直接决定整个BST的健壮性空树插入当root nullptr时Insert必须创建新节点并赋值给root。很多新手写成Node* newNode new Node(val);却忘了root newNode;导致插入后root仍是空指针。根节点插入当树非空但目标值恰好等于根节点值且策略为REJECTInsert必须立即返回不能继续向下遍历。否则会触发空指针解引用。一个经过千次压测验证的Insert迭代实现如下支持DuplicatePolicybool insert_iterative(Node* root, int val, DuplicatePolicy policy) { // 处理空树情况必须用引用传参才能修改root指针本身 if (root nullptr) { root new Node(val); return true; } Node* current root; Node* parent nullptr; bool go_left false; // 寻找插入位置 while (current ! nullptr) { parent current; if (val current-val) { current current-left; go_left true; } else if (val current-val) { current current-right; go_left false; } else { // val current-val遇到重复值 switch (policy) { case DuplicatePolicy::REJECT: return false; // 拒绝插入 case DuplicatePolicy::INSERT_RIGHT: current current-right; go_left false; break; case DuplicatePolicy::INSERT_LEFT: current current-left; go_left true; break; case DuplicatePolicy::COUNT: current-count; return true; } } } // current为nullptrparent为插入位置的父节点 Node* newNode new Node(val); if (go_left) { parent-left newNode; } else { parent-right newNode; } return true; }关键点解析Node* root根指针必须用引用传递否则空树插入时无法修改外部root变量。go_left标志记录最后一步是向左还是向右移动决定新节点挂在父节点的左还是右分支。COUNT策略直接在原节点操作不创建新节点零内存分配。3.3 Insert的性能暗礁随机插入 vs 有序插入的灾难性对比Insert的输入数据分布对BST形态有决定性影响。我做过一组实验向空BST插入10000个随机整数 vs 插入10000个升序整数。输入类型平均Search耗时纳秒树高内存占用KB备注随机整数42014128接近理想平衡升序整数1860010000132退化为单链表升序插入的后果触目惊心树高从O(log n)恶化为O(n)Search从对数时间变成线性扫描。更糟的是这种退化是静默的——程序不会崩溃只是越来越慢直到某天用户投诉“系统卡顿”。解决方案不是禁止有序数据而是主动干预方案1Shuffle预处理在Insert大批量数据前先将数组随机打乱。STL的std::shuffle可轻松实现时间复杂度O(n)远低于Insert的O(n log n)总成本。方案2批量构建Bulk Build如果数据已知且静态不用逐个Insert而是先排序再用“中序遍历构造平衡BST”算法一次性构建。核心思想取排序数组中点为根左半部分递归建左子树右半部分递归建右子树。时间复杂度O(n)树高严格为⌊log₂n⌋1。// 从已排序数组批量构建平衡BST Node* build_balanced_bst(const std::vectorint sorted, int left, int right) { if (left right) return nullptr; int mid left (right - left) / 2; Node* root new Node(sorted[mid]); root-left build_balanced_bst(sorted, left, mid - 1); root-right build_balanced_bst(sorted, mid 1, right); return root; }经验之谈在配置加载、日志回放、测试数据初始化等场景永远优先选择Bulk Build而非循环Insert。前者是“建设”后者是“堆砌”结果天壤之别。4. Remove操作BST的成人礼也是最容易崩塌的环节如果说Search是BST的呼吸Insert是它的进食那么Remove就是它的新陈代谢——最基础也最危险。BST的Remove之所以难并非因为代码行数多而在于它强制你直面数据结构的结构性矛盾删除一个节点意味着要从树中物理移除一个连接点但BST的搜索性质要求“左小右大”的拓扑关系必须全局保持。这就逼你在局部手术中完成一场精密的全局重构。我见过最惨烈的Remove事故发生在某支付网关的风控规则树上。开发同学实现了单子节点删除即目标节点只有左或右孩子但漏掉了双子节点删除逻辑。上线后当某条高危规则被Remove系统误将右子树的最小节点后继作为新根却忘了断开它与原父节点的连接导致右子树被整体“悬空”后续所有Search都返回空风控规则全面失效持续17分钟。4.1 Remove的三大分支为什么不能只写一种情况BST Remove必须处理三种互斥情况缺一不可Case 1叶子节点无子节点最简单直接删除将其父节点对应指针置为nullptr。Case 2单子节点只有左或右孩子将孩子节点“上移”替代被删节点即让父节点直接指向该孩子。Case 3双子节点左右孩子均非空最复杂必须找到一个语义等价的节点来填补空缺且不破坏BST性质。这就是后继successor或前驱predecessor登场的时刻。很多教程只强调“用后继替换”却没说清为什么是后继而不是其他节点。答案藏在BST的定义里被删节点N的左子树所有值 N.val右子树所有值 N.val。要找一个值来填N的位置这个值必须满足大于左子树所有值且小于右子树所有值。显然右子树的最小值即后继完美符合——它比左子树所有值大因在右子树又比右子树其他值小因是最小值。同理左子树的最大值前驱也满足条件。选择后继是惯例选择前驱同样正确但必须统一否则树的演化逻辑会混乱。4.2 后继节点的定位与安全替换三步原子操作找到后继只是第一步如何安全地把它“搬”到被删节点的位置才是Remove的精髓。这绝不是简单的target-val successor-val而是涉及指针重连的三步原子操作定位后继从被删节点的右子节点出发一路向左直到left nullptr。此时的节点即为后继。摘除后继后继节点必为叶子或仅有右孩子因为它没有左孩子。按Case 1或Case 2逻辑删除它。值迁移与指针嫁接将后继的值复制到被删节点然后将被删节点的左右指针完整“继承”后继节点的左右指针注意后继已被摘除其指针是干净的。以下是完整的Remove实现迭代版避免递归栈溢出bool remove(Node* root, int val) { // Step 1: Search for the node to remove, with parent tracking auto [target, parent] search_iterative(root, val); if (target nullptr) return false; // Not found // Step 2: Handle three cases if (target-left nullptr target-right nullptr) { // Case 1: Leaf node _remove_leaf(root, target, parent); } else if (target-left nullptr || target-right nullptr) { // Case 2: One child _remove_one_child(root, target, parent); } else { // Case 3: Two children - use successor _remove_two_children(root, target, parent); } return true; } // Helper: Remove leaf node void _remove_leaf(Node* root, Node* target, Node* parent) { if (parent nullptr) { // target is root delete target; root nullptr; } else if (parent-left target) { parent-left nullptr; delete target; } else { parent-right nullptr; delete target; } } // Helper: Remove node with one child void _remove_one_child(Node* root, Node* target, Node* parent) { Node* child (target-left ! nullptr) ? target-left : target-right; if (parent nullptr) { // target is root root child; } else if (parent-left target) { parent-left child; } else { parent-right child; } delete target; } // Helper: Remove node with two children void _remove_two_children(Node* root, Node* target, Node* parent) { // Find successor: smallest in right subtree Node* successor target-right; Node* succ_parent target; while (successor-left ! nullptr) { succ_parent successor; successor successor-left; } // Copy successors value to target target-val successor-val; // Now remove the successor (it has at most one child) if (succ_parent-left successor) { _remove_one_child(root, successor, succ_parent); } else { // successor is target-right itself _remove_one_child(root, successor, target); } }关键洞察_remove_two_children中我们不删除target节点而是删除successor节点并把successor的值拷贝给target。这样做有两大好处(1) 避免了target节点指针重连的复杂逻辑(2) 保证了target节点的内存地址不变这对上层持有该节点指针的代码如缓存、观察者列表极其友好。这是一种典型的“以空间换稳定”的工程智慧。4.3 Remove的并发安全为什么全局锁是毒药而细粒度锁是解药在多线程环境中BST的Search/Insert/Remove必须考虑线程安全。一个常见误区是给整个BST加一把全局互斥锁mutex。这在高并发下是性能杀手——所有线程排队等待同一把锁吞吐量骤降。更优的方案是锁粒度下沉到节点级别。核心思想每个节点持有一个读写锁如std::shared_mutexSearch用共享锁允许多个并发读Insert/Remove用独占锁排他写。当操作一个节点时只需锁住该节点及其父节点因为Insert/Remove会修改父节点的指针。以下是一个简化的线程安全Remove框架class ThreadSafeBST { private: struct Node { int val; std::shared_mutex mutex; // 每个节点独立的读写锁 Node* left; Node* right; }; Node* root; // 递归获取路径上所有节点的锁从根到目标 std::vectorstd::unique_lockstd::shared_mutex acquire_path_locks(Node* target, Node* parent) { std::vectorstd::unique_lockstd::shared_mutex locks; // 锁父节点写操作需要 if (parent) locks.emplace_back(parent-mutex, std::defer_lock); // 锁目标节点读写都需要 locks.emplace_back(target-mutex, std::defer_lock); // 按顺序加锁避免死锁 for (auto lock : locks) lock.lock(); return locks; } public: bool remove(int val) { auto [target, parent] search_iterative(root, val); if (!target) return false; auto locks acquire_path_locks(target, parent); // ... 执行_remove_xxx系列操作此时已加锁 return true; } };实战提醒细粒度锁虽好但增加了复杂度。对于QPS1000的内部服务全局锁完全够用只有在高频交易、实时风控等微秒级延迟敏感场景才值得投入精力实现节点级锁。切勿为“技术正确”而过度设计。5. 工程落地 checklist从代码到生产环境的12个生死关卡写完Search/Insert/Remove的代码只是万里长征第一步。真正的挑战在于如何让这套逻辑在生产环境里7x24小时稳定运行。以下是我在多个项目中总结出的12个必检关卡每一个都曾是线上事故的源头5.1 内存管理new/delete的配对与泄漏检测BST节点动态分配new和delete必须严格配对。我曾用Valgrind抓到一个经典泄漏Remove操作中当删除双子节点时代码正确摘除了后继节点但忘记delete被删的target节点只拷贝了值导致target节点内存永久泄漏。Checklist✅ 所有new Node必须有且仅有一个对应的delete。✅ 使用智能指针std::unique_ptrNode自动管理但需注意循环引用风险父指针不能用shared_ptr。✅ 上线前用AddressSanitizerASan编译捕获use-after-free和heap-buffer-overflow。5.2 边界值测试INT_MIN/INT_MAX的比较陷阱整数BST中当val INT_MIN时val current-val比较可能因溢出失效。例如current-val INT_MIN 1val - current-val会溢出为正数导致错误走向右子树。Checklist✅ 对所有比较操作用和替代和避免减法溢出。✅ 单元测试必须覆盖INT_MIN、INT_MAX、0、-1等边界值。5.3 树高监控为BST装上“血压计”BST性能退化是渐进式的必须主动监控。我在所有BST实例中植入了get_height()和is_balanced()方法并通过Prometheus暴露为指标。int get_height(Node* root) { if (!root) return 0; return 1 std::max(get_height(root-left), get_height(root-right)); } // 检查是否平衡AVL风格左右子树高度差1 bool is_balanced(Node* root) { if (!root) return true; int left_h get_height(root-left); int right_h get_height(root-right); return std::abs(left_h - right_h) 1 is_balanced(root-left) is_balanced(root-right); }Checklist✅ 每5分钟采集一次树高当height 2 * floor(log2(size)) 1时触发告警。✅is_balanced()不用于实时判断太慢仅用于故障复盘和定期巡检。5.4 日志埋点让每一次Search都可追溯Search操作本身不修改状态但它是系统健康度的晴雨表。我在Search入口添加了结构化日志// 日志格式[BST_SEARCH] key12345 path_len7 hittrue time_us12.3 LOG_INFO(BST_SEARCH key%d path_len%d hit%s time_us%.1f, val, path_length, hit ? true : false, elapsed_us);Checklist✅path_length记录搜索经过的节点数是树平衡性的直接反映。✅hit标识是否命中用于分析缓存穿透率。✅time_us微秒级耗时设置P99阈值告警如100us。5.5 压测验证用真实流量击穿你的BST单元测试只能覆盖逻辑分支压测才能暴露性能瓶颈。我用wrk模拟1000并发持续发送随机Search请求观察✅ CPU使用率是否随并发线性增长应接近线性。✅ P99延迟是否稳定波动不应超过±20%。✅ 内存RSS是否持续上涨泄漏迹象。关键发现在某次压测中P99延迟在并发800时突增至200ms排查发现是get_height()被误放在Search热路径中——这个O(n)操作本该只在后台线程调用。教训任何O(n)操作都不得进入O(log n)的热路径。5.6 故障演练主动删除根节点看系统是否还能呼吸最残酷的测试就是模拟最坏场景。我定期执行“混沌工程”remove(root-val)—— 删除根节点验证树是否自动重组。remove(INT_MAX)—— 删除最大值验证右子树是否完整迁移。insert(INT_MIN); insert(INT_MAX);—— 极端值插入验证比较逻辑。Checklist✅ 每次故障演练后用inorder_traversal()输出中序序列确认仍为升序。✅ 检查root指针是否非空且root-left/root-right指向有效内存。5.7 序列化备份BST不是黑匣子必须能存能取BST在进程重启后会丢失必须支持序列化。我采用“中序前序”双序列化方案可在O(n)时间内重建原树无需排序。// 中序序列化升序排列 void inorder_serialize(Node* root, std::vectorint result) { if (!root) return; inorder_serialize(root-left, result); result.push_back(root-val); inorder_serialize(root-right, result); } // 前序序列化根-左-右 void preorder_serialize(Node* root, std::vectorint result) { if (!root) return; result.push_back(root-val); preorder_serialize(root-left, result); preorder_serialize(root-right, result); }Checklist✅ 序列化数据存入本地文件或Redis进程启动时自动加载。✅ 加入CRC32校验防止文件损坏导致重建失败。5.8 API契约明确定义Search/Insert/Remove的异常行为API文档必须白纸黑字写清Search未找到时返回nullptr永不抛异常。Insert重复值策略为REJECT时返回falseCOUNT时返回true。Remove目标不存在时返回false成功删除返回true。Checklist✅ 所有API的返回值含义在头文件注释中用Doxygen格式精确描述。✅ 禁止在BST内部抛出异常如std::bad_alloc统一由上层捕获处理。5.9 编译器优化禁用可能导致UB的优化选项GCC/Clang的-O3可能对指针操作做激进优化导致UBUndefined Behavior。我在CMakeLists.txt中强制添加# 禁用可能导致UB的优化 if(CMAKE_CXX_COMPILER_ID MATCHES GNU|Clang) set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -fno-strict-aliasing) endif()Checklist✅ 使用-fsanitizeundefined编译捕获未定义行为。✅ 禁用-fltoLink Time Optimization避免跨编译单元的指针优化错误。5.10 文档即代码用doctest编写可执行文档最好的文档是能运行的代码。我用doctest框架把API用法写成测试TEST_CASE(BST Remove handles two children correctly) { BST bst; bst.insert(50); bst.insert(30); bst.insert(70); bst.insert(20); bst.insert(40); bst.insert(60); bst.insert(80); // Remove root (50), should be replaced by successor (60) bst.remove(50); REQUIRE(bst.search(60) true); // 60 now at root REQUIRE(bst.search(50) false); }Checklist