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

目录

插入的本质是什么?

[如何寻找"合法"的位置?------ 模拟查找过程](#如何寻找“合法”的位置?—— 模拟查找过程)

[递归插入(Recursive Insert)------ 优雅的实现](#递归插入(Recursive Insert)—— 优雅的实现)

代码逐步完善

总结


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

插入的本质是什么?

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

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

cpp 复制代码
    15
   /  \
  8    20
 / \
3   12

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

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

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

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

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


如何寻找"合法"的位置?------ 模拟查找过程

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

cpp 复制代码
    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)。

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

cpp 复制代码
#include <cstddef> // 为了 NULL

struct 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,我们就需要决定是往左还是往右。

cpp 复制代码
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

cpp 复制代码
    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)的地址没有变,所以我们要把它返回给它的上层调用,以保持树的连接。

cpp 复制代码
/*
 * 函数功能:向以 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 函数的返回值,以防根节点发生改变(比如第一次插入时)。

cpp 复制代码
#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);

    // 现在我们插入新值 10
    std::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;

    // 尝试插入一个重复的值 12
    root = 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, ...) 是实现连接的核心。它用递归调用的返回值(可能是新节点,也可能是原来的子树根)来更新自己的孩子指针。

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

相关推荐
姜不吃葱7 分钟前
【力扣热题100】双指针—— 接雨水
数据结构·算法·leetcode·力扣热题100
拂晓银砾24 分钟前
Java数据结构-队列
java·数据结构
John.Lewis25 分钟前
数据结构初阶(19)外排序·文件归并排序的实现
c语言·数据结构·排序算法
John.Lewis30 分钟前
数据结构初阶(16)排序算法——归并排序
c语言·数据结构·排序算法
tanyongxi661 小时前
C++ 特殊类设计与单例模式解析
java·开发语言·数据结构·c++·算法·单例模式
闪电麦坤952 小时前
数据结构:用链式队列实现层序遍历 (Level-order Traversal)
数据结构··遍历
星星火柴93612 小时前
关于“双指针法“的总结
数据结构·c++·笔记·学习·算法
艾莉丝努力练剑13 小时前
【洛谷刷题】用C语言和C++做一些入门题,练习洛谷IDE模式:分支机构(一)
c语言·开发语言·数据结构·c++·学习·算法
闪电麦坤9514 小时前
数据结构:迭代方法(Iteration)实现树的遍历
数据结构·二叉树·