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

数据结构:在二叉搜索树中插入元素(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.xdnf.cn/news/18058.html

相关文章:

  • 已开源:Highcharts.NET,Highcharts Android,与Highcharts iOS集成
  • JUC常用线程辅助类详解
  • Vue中v-show与v-if的区别
  • 【每日一题】Day 6
  • 代码管理系统简介与部署
  • 2.4 双向链表
  • Redis学习--集群 数据分片、哈希槽、集群配置、主从容错迁移、扩缩容
  • Golang 后台技术面试套题 1
  • 《Nursing Research》(护理SCI)LaTeX模板详细教程:从入门到投稿(一)
  • OpenMemory MCP发布!AI记忆本地共享,Claude、Cursor一键同步效率翻倍!
  • Week 12: 深度学习补遗:RNN与LSTM
  • Go语言并发编程 ------ 锁机制详解
  • 【C++知识杂记1】智能指针及其分类
  • w嵌入式分享合集68
  • 什么是EDA(Exploratory Data Analysis,探索性数据分析)
  • MariaDB 多源复制
  • Windchill 11 Enumerated Type Customization Utility-枚举类型自定义实用程序
  • 嵌入式开发入门—电子元器件~半导体
  • Linux设备模型深度解析
  • 运动场和光流-动手学计算机视觉17
  • Spring 源码学习(十一)—— webmvc 配置
  • 【k8s、docker】Headless Service(无头服务)
  • Tomcat Connector连接器原理
  • 阶段二:7-上网行为安全概述
  • Spring Boot 项目配置 MySQL SSL 加密访问
  • SQL详细语法教程(四)约束和多表查询
  • 智能汽车领域研发,复用云原始开发范式?
  • 开源数据发现平台:Amundsen Search Service 搜索服务
  • SparkSQL性能优化实践指南
  • gRPC网络模型详解