当前位置: 首页 > news >正文

数据结构:在二叉搜索树中插入元素(Insert in a BST)

目录

插入的本质是什么?

如何寻找“合法”的位置?—— 模拟查找过程

递归插入(Recursive Insert)—— 优雅的实现

代码逐步完善

总结


上一节我们从第一性原理搞清楚了二叉搜索树(BST)是什么,以及为什么要有它。现在,我们来解决下一个核心问题:如何向一个已有的 BST 中插入一个新元素?

插入的本质是什么?

我们有一个新数字,比如 10,要把它插入到上次创建的这棵树里:

数据结构:二叉搜索树(Binary Search Tree)-CSDN博客

    15/  \8    20/ \
3   12

在动手之前,我们必须牢记 BST 的唯一天条(The Golden Rule):

对于任何节点,其左子树的所有值都比它小,右子树的所有值都比它大。

所以,插入一个新节点,本质上就两个步骤:

  1. 找到一个“合法”的位置:这个位置必须能容纳新节点,并且新节点放进去之后,整棵树依然要满足 BST 的“天条”。

  2. 执行插入动作:把新节点安家落户在这个位置上。


如何寻找“合法”的位置?—— 模拟查找过程

这个寻找位置的过程,其实和我们查找一个数的过程一模一样。我们就像这个新来的数字 10,要在这棵树里找个家。

    15/  \8    20/ \
3   12

从根节点 15 开始问路:

  • 10 小于 15 (10 < 15)。

  • 根据天条,10 的家一定在 15左子树里。我们往左走。

来到节点 8

  • 10 大于 8 (10 > 8)。

  • 根据天条,10 的家一定在 8右子树里。我们往右走。

来到节点 12

  • 10 小于 12 (10 < 12)。

  • 根据天条,10 的家一定在 12左子树里。我们往左走。

12 的左边是什么?

  • NULL!一个空位!

  • 这是一个“死胡同”,我们没法再往下走了。

这个“死胡同”(NULL 指针)就是我们梦寐以求的“合法”位置。为什么?

  • 把它放在这里,它会成为 12 的左孩子。因为 10 < 12,这满足了对于节点 12 的天条。

  • 同时,10 > 810 < 15,它也满足了对于它所有祖先节点的天条。

  • 最重要的是,把它放在一个 NULL 的位置,作为一个新的叶子节点,我们不需要改动树中任何其他节点的现有连接关系,这是成本最低的操作!

 新节点插入的位置,必然是树中某个节点的 NULLleftright 指针所在的位置。寻找这个位置的过程,就是一个模拟查找的过程。


递归插入(Recursive Insert)—— 优雅的实现

现在我们用代码来实现这个逻辑。递归是处理树结构最自然、最优雅的方式。

递归的思想: 我们把一个大问题,分解成一个性质相同、但规模更小的子问题。 对于插入操作:

  • 大问题:在以 root 为根的整棵树中插入 value

  • 子问题

    • 如果 value < root->data,问题就缩小为:在以 root->left 为根的左子树中插入 value

    • 如果 value > root->data,问题就缩小为:在以 root->right 为根的右子树中插入 value

这个分解过程什么时候结束呢?—— 当我们遇到一个 NULL 的节点时,也就是找到了那个“死胡同”,这就是递归的基本情况(Base Case)

代码逐步完善

函数签名和基本情况 (Base Case)

首先,我们需要一个函数。它应该做什么?

  • 它需要知道从哪个节点开始(Node* root)。

  • 它需要知道要插入的值是多少(int value)。

  • 当插入完成后,树的结构可能发生变化(比如,从一棵空树变成有一个节点的树),所以这个函数必须返回新树的根节点指针。

#include <cstddef> // 为了 NULLstruct Node {int data;Node* left;Node* right;Node(int value) {data = value;left = right = NULL;}
};/** 函数功能:向以 root 为根的树中插入 value* 返回值:  返回插入新节点后,树(或子树)的根节点*/
Node* insert(Node* root, int value) {// 基本情况 (Base Case):// 如果当前的 root 是 NULL (无论是整棵树为空,还是我们已经走到了一个“死胡同”)// 这就是我们要插入新节点的地方。if (root == NULL) {// 创建一个新节点,它自己就是一棵子树return new Node(value);}// ... 递归的步骤还没写 ...
}

思考一下 return new Node(value); 这一行。

它返回一个指向新创建节点(值为value)的指针。谁会接收这个指针呢?是上一层的函数调用。我们马上就能看到。

加入递归步骤

现在,如果 root 不是 NULL,我们就需要决定是往左还是往右。

Node* insert(Node* root, int value) {if (root == NULL) {return new Node(value);}// 递归步骤 (Recursive Step):if (value < root->data) {// 新值应该插入到左子树中// 我们递归调用 insert,让它去处理左子树的问题// **关键一步**: 递归调用返回的结果(可能是新的左子树的根)//            必须被连接到当前节点的左指针上!root->left = insert(root->left, value);} else if (value > root->data) {// 新值应该插入到右子树中// 同理,连接到当前节点的右指针上root->right = insert(root->right, value);}// ... 还有一种情况:value == root->data ...// ... 以及,这个函数在递归调用后应该返回什么?...
}

我们来剖析 root->left = insert(root->left, value); 这句代码,它是递归插入的精髓。

假设我们要在节点 12 的左边插入 10

    15/  \8    20/ \
3   12
  • 当前 root12value10

  • 10 < 12,所以执行 root->left = insert(root->left, value);

  • 此时 root->leftNULL。所以 insert(NULL, 10) 被调用。

  • 根据我们的基本情况,insert(NULL, 10)new Node(10) 并返回指向这个新节点的指针。

  • 这个返回的指针,被赋值给了 root->left (也就是 12left 指针)。

  • 效果达成:新节点 10 被成功地连接为了 12 的左孩子。

处理重复值并完成函数

标准的 BST 通常不允许重复值。如果待插入的值和当前节点的值相等,我们什么都不做,直接返回。

最后,如果当前节点不是 NULL,并且完成了对子树的插入操作后,当前节点本身(root)的地址没有变,所以我们要把它返回给它的上层调用,以保持树的连接。

/** 函数功能:向以 root 为根的树中插入 value* 返回值:  返回插入新节点后,树(或子树)的根节点*/
Node* insert(Node* root, int value) {// 基本情况:如果当前节点是 NULL,说明找到了插入位置。if (root == NULL) {// 创建新节点并返回,上一层的调用会把它连接到树中。return new Node(value);}// 递归步骤:if (value < root->data) {// 向左子树递归插入root->left = insert(root->left, value);} else if (value > root->data) {// 向右子树递归插入root->right = insert(root->right, value);}// else (value == root->data) 的情况:// 我们选择什么都不做,因为不允许重复值。// 将当前的(可能未改变的)根节点指针返回给上一层。// 这一步确保了树中未受影响部分的连接保持不变。return root;
}

如何使用这个函数?

主调函数需要一个 Node* 变量来保存整棵树的根。每次插入,都需要用这个变量来接收 insert 函数的返回值,以防根节点发生改变(比如第一次插入时)。

#include <iostream>// (这里是 Node 结构体和 insert 函数的定义)// 为了方便观察,我们写一个中序遍历函数来打印树
void inorderTraversal(Node* root) {if (root == NULL) {return;}inorderTraversal(root->left);std::cout << root->data << " ";inorderTraversal(root->right);
}int main() {Node* root = NULL; // 一开始,树是空的// 第一次插入,root 会从 NULL 变成新节点的地址root = insert(root, 15);root = insert(root, 8);root = insert(root, 20);root = insert(root, 3);root = insert(root, 12);// 现在我们插入新值 10std::cout << "插入 10 之前的中序遍历: ";inorderTraversal(root); // 输出: 3 8 12 15 20 std::cout << std::endl;root = insert(root, 10);std::cout << "插入 10 之后的中序遍历: ";inorderTraversal(root); // 输出: 3 8 10 12 15 20 std::cout << std::endl;// 尝试插入一个重复的值 12root = insert(root, 12);std::cout << "插入重复值 12 之后的中序遍历: ";inorderTraversal(root); // 输出: 3 8 10 12 15 20 (没有变化)std::cout << std::endl;return 0;
}

注意:inorderTraversal 只是为了验证我们插入的正确性,它本身也是递归的一个经典应用。


总结

  • 第一性原理:插入操作必须维护 BST 的“左小右大”核心性质。

  • 推导:最简单、成本最低的插入位置是某个叶子节点的 NULL 孩子位置。这个位置可以通过模拟“查找”过程来找到。

  • 递归实现

    • Base Case (基本情况):当前节点为 NULL。这是递归的终点,也是新节点被创建和“返回”的地方。

    • Recursive Step (递归步骤):比较新值和当前节点的值,决定向左还是向右“委托”任务(递归调用)。

    • 关键连接root->left = insert(root->left, ...) 是实现连接的核心。它用递归调用的返回值(可能是新节点,也可能是原来的子树根)来更新自己的孩子指针。

    • 返回值:函数必须返回根节点指针,以确保调用者能正确地更新树的结构。

http://www.dtcms.com/a/336266.html

相关文章:

  • linux-高级IO(上)
  • 猫头虎AI分享|一款Coze、Dify类开源AI应用超级智能体Agent快速构建工具:FastbuildAI
  • #买硬盘欲安装k8s记
  • Flutter 3.35 更新要点解析
  • ICCV 2025 | Reverse Convolution and Its Applications to Image Restoration
  • 如何运用好DeepSeek为自己服务:智能增强的范式革命 1.2 DeepSeek认知增强模型
  • 计算机基础速通--数据结构·图的基础应用三(基础图算法进阶)
  • Tauri 框架介绍
  • 《Nursing Research》(护理SCI)LaTeX模板详细教程:从入门到投稿(一)
  • 炒股术语:“洗盘”
  • LLM入门学习
  • 【165页PPT】锂电池行业SAP解决方案(附下载方式)
  • 【C++】Windows 下 TCP接口超详介绍,如何实现一个TCP服务端和客户端
  • 又一家茑屋书店即将歇业,精品书店的未来在哪里?
  • ruoyi-vue(十一)——代码生成
  • Python爬虫实战:研究optimesh库,构建Github网格数据采集系统
  • AntSK-PyAPI技术深度解析:打造企业级文本嵌入向量服务的完整指南
  • C语言(12)——进阶函数
  • 【Linux】文件基础IO
  • 软件需求High-Level(高层级)需求和Low-Level(低层级)需求
  • rt-thread audio框架移植stm32 adc+dac,用wavplayer录音和播放
  • 测试Windows10IoT系统是否可以正常运行KingSCSDA3.8软件
  • python的软件工程与项目管理课程组学习系统
  • 算法第四十八天:单调栈part01(第十章)
  • C++ 力扣 904.水果成篮 题解 优选算法 滑动窗口 每日一题
  • 算法03 归并分治
  • 最优化:建模、算法与理论|02 Optimization Modeling and Typical Examples(1)
  • Linux:TCP协议
  • 时间复杂度、空间复杂度和渐近符号(O、Ω、Θ 等)
  • Vue深入组件:组件注册详解