二叉搜索树 BST 三板斧:查、插、删的底层逻辑

二叉搜索树:从定义到实战操作全解析 🌳

一、什么是二叉搜索树?📚

二叉搜索树(Binary Search Tree,简称 BST)是一种特殊的二叉树,它的存在让数据的查找、插入和删除操作变得高效又有序。具体来说,它有两个核心定义:

  • 要么是一棵空树;
  • 要么是由根节点、左子树和右子树组成的树,且左子树和右子树都是二叉搜索树,左子树的所有节点值都小于根节点值,右子树的所有节点值都大于根节点值(左子树 < 根节点 < 右子树)。

正因为这种严格的大小关系,二叉搜索树也被称为 "排序二叉树"。它的一个神奇特性是:中序遍历的结果是严格递增的有序序列 !这让它在需要有序数据的场景中大放异彩。

例如:

复制代码
        6
      /   \
     3     8
    / \   / \
   1   4 7   9

对这棵 BST 进行中序遍历,结果为:1 → 3 → 4 → 6 → 7 → 8 → 9,完美呈现递增有序。

从时间复杂度来看,二叉搜索树的平均操作效率是 O (logn)(类似二分查找),但在最坏情况下(比如所有节点都只有左子树或右子树,变成 "斜树"),效率会退化为 O (n)。不过总体来说,它依然是支持高效查找的得力助手~

二、二叉搜索树的相关操作 🔧

二叉搜索树的核心操作有三个,也是我们日常使用中最频繁的功能:

  • 查找数据域为某一特定值的节点 🔍
  • 插入一个新的节点 ➕
  • 删除一个指定节点 ➖

接下来我们就逐一拆解这些操作的逻辑和实现~

三、查找数据域为某一特定值的节点 🔍

查找是二叉搜索树最基础的操作,得益于它 "左小右大" 的特性,我们可以像 "走捷径" 一样定位目标节点。

查找步骤:

  1. 起始:从树的根节点开始(如果根节点为空,直接返回 null,说明树中无数据);

  2. 循环对比 + 定向查找

    • 若当前节点值等于目标值:找到啦!返回该节点;
    • 若当前节点值大于目标值:目标一定在左子树(因为左子树都比根小),更新当前节点为左子节点;
    • 若当前节点值小于目标值:目标一定在右子树(因为右子树都比根大),更新当前节点为右子节点;
  3. 终止:要么找到目标节点,要么遍历到 null(说明目标不存在)。

核心逻辑:

靠 BST 的有序性 "精准导航",不盲目遍历,只走一条路,全程无回溯,效率自然高~

代码实现:

javascript

复制代码
// 查找值为n的节点
function search(root, n) {
    // 根节点为空,直接返回(树中无数据)
    if (!root) {
        return null;
    }

    // 找到目标节点,返回该节点
    if (root.val === n) {
        return root;
    } else if (root.val > n) {
        // 当前节点值大于目标值,去左子树找
        return search(root.left, n);
    } else {
        // 当前节点值小于目标值,去右子树找
        return search(root.right, n);
    }
}

// 测试用例:构造一棵二叉搜索树
class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

const root = new TreeNode(6);
root.left = new TreeNode(3);
root.right = new TreeNode(8);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(7);
root.right.right = new TreeNode(9);

console.log(search(root, 4)); // TreeNode { val: 4, left: null, right: null }

四、插入一个新的节点 ➕

插入操作的核心是 "找到唯一合法的空位",并保证插入后依然满足 BST 的有序性。

插入步骤:

  1. 起始:从树的根节点开始;

  2. 循环对比 + 找插入空位

    • 若当前节点值大于新节点值:往左子树走(更新当前节点为左子节点);
    • 若当前节点值小于新节点值:往右子树走(更新当前节点为右子节点);
    • 一直走直到遇到 null(这个 null 就是唯一合法的插入位置);
  3. 插入 + 终止:把新节点挂在这个 null 的位置(作为对应父节点的左 / 右子节点),插入完成。

核心逻辑:

新节点一定是 "叶子节点"(即没有子节点),因为我们是沿着 BST 的规则找到最末端的空位,不会破坏原有节点的结构和大小关系。

代码实现:

javascript

复制代码
// 向BST中插入值为n的节点
function insertIntoBst(root, n) {
    // 若根节点为空,直接返回新节点(树为空时,新节点就是根)
    if (!root) {
        return new TreeNode(n);
    }
    
    // 不考虑值相等的情况(BST通常不允许重复值,若需支持可额外处理)
    if (root.val > n) {
        // 当前节点值大于新值,往左子树插入(递归更新左子树)
        root.left = insertIntoBst(root.left, n);
    } else {
        // 当前节点值小于新值,往右子树插入(递归更新右子树)
        root.right = insertIntoBst(root.right, n);
    }
    // 返回更新后的根节点
    return root;
}

为什么 "只能在原树叶子节点后插入新节点"?

  1. 原树的非叶子节点已有子节点,无合法空位容纳新节点,直接插入会违背二叉树 / BST 的结构规则,破坏有序性和数据完整性,增加插入的难度;
  2. 任何合法的 BST 插入,都必须依托 "原树的末端空位"(即原树叶子节点的左 / 右子节点空位,或原树的空节点),这个空位是唯一无需改动原有节点就能容纳新节点的位置;
  3. 在原树叶子节点后插入,既能保证新节点插入时是叶子节点(无子女,不冲击原有 BST 规则),又能保证不改动任何原有节点的结构,维持 O (logn) 的高效插入效率,这是保证 BST 有序性和高效性的唯一最优解

五、删除一个指定节点 ➖

删除操作是 BST 中最复杂的操作,因为删除节点后需要维持 "左小右大" 的规则,需要分情况处理。

操作步骤:

  1. 找到目标节点:从根节点开始,根据值的大小向左 / 右子树查找,直到找到值等于目标值的节点;
  2. 按 3 种情况处理目标节点(核心中的核心):
情况 1:目标节点是叶子节点(无左右子树)

直接把它置为 null 即可(父节点的对应指针指向 null),因为删除叶子节点不会影响其他节点的大小关系。

情况 2:目标节点有左子树(无论有没有右子树)

用左子树中最大的节点替换目标节点的值(左子树最大节点是最右侧的节点),然后删除左子树中这个最大节点(此时这个最大节点一定是叶子节点或只有左子树,递归处理即可)。

情况 3:目标节点只有右子树(无左子树)

用右子树中最小的节点替换目标节点的值(右子树最小节点是最左侧的节点),然后删除右子树中这个最小节点(同理,这个节点也容易处理)。

核心总结:

先定向找到目标节点,再按 "叶子直接删、有左找左最大、仅右找右最小" 的规则替换 + 删冗余,全程不破坏 BST 有序性。

代码实现:

javascript

复制代码
// 删除值为n的节点
function deleteNode(root, n) {
    // 根节点为空,直接返回
    if (!root) {
        return null;
    }

    // 找到目标节点
    if (root.val === n) {
        // 情况1:叶子节点(无左右子树)
        if (!root.left && !root.right) {
            root = null;
        } else if (root.left) {
            // 情况2:有左子树(无论是否有右子树)
            // 找左子树最大节点
            const maxLeft = findMax(root.left);
            // 用最大值替换当前节点值
            root.val = maxLeft.val;
            // 递归删除左子树中这个最大节点
            root.left = deleteNode(root.left, maxLeft.val);
        } else {
            // 情况3:只有右子树(无左子树)
            // 找右子树最小节点
            const minRight = findMin(root.right);
            // 用最小值替换当前节点值
            root.val = minRight.val;
            // 递归删除右子树中这个最小节点
            root.right = deleteNode(root.right, minRight.val);
        }
    } else if (root.val > n) {
        // 目标在左子树,递归删除左子树
        root.left = deleteNode(root.left, n);
    } else {
        // 目标在右子树,递归删除右子树
        root.right = deleteNode(root.right, n);
    }
    return root;
}

// 寻找左子树最大值(最右侧节点)
function findMax(root) {
    while (root.right) {
        root = root.right;
    }
    return root;
}

// 寻找右子树最小值(最左侧节点)
function findMin(root) {
    while (root.left) {
        root = root.left;
    }
    return root;
}

思考与拓展:

1.目标节点有左子树且有右子树时,也可以用右子树里的最小值来替换目标节点的值,再去右子树里删除这个最小值节点,那为什么情况2与情况3不能合并?

情况 2 的条件是 "有左子树"(可能同时有右子树),情况 3 的条件是 "只有右子树"(无左子树),两者的处理逻辑不同:

  • 情况 2 需要从左子树找最大值(左子树存在的前提下);
  • 情况 3 只能从右子树找最小值(因为左子树不存在)。

如果合并,比如统一用右子树最小值处理,当节点只有左子树时(无右子树),就会出现 "右子树为 null" 的错误。

举个例子:若目标节点是值为 3 的节点(左子树有 1,无右子树),此时只能用左子树最大值(1)替换;若强行找右子树最小值,会因右子树为 null 报错。因此必须分开处理~

2. 可不可以让左子树存在时优先用右子树最小值?

当然可以!只要保证替换后依然满足 BST 规则即可。此时情况划分变为:

  • 情况 1:叶子节点→直接删;
  • 情况 2:有右子树(无论有没有左子树)→用右子树最小值替换,再删该最小值节点;
  • 情况 3:只有左子树(无右子树)→用左子树最大值替换,再删该最大值节点。

对应的代码实现:

javascript

复制代码
function deleteNodeV2(root, n) {
    if (!root) return null;

    if (root.val === n) {
        // 情况1:叶子节点
        if (!root.left && !root.right) {
            root = null;
        } else if (root.right) {
            // 情况2:有右子树(无论是否有左子树)
            const minRight = findMin(root.right);
            root.val = minRight.val;
            root.right = deleteNodeV2(root.right, minRight.val);
        } else {
            // 情况3:只有左子树(无右子树)
            const maxLeft = findMax(root.left);
            root.val = maxLeft.val;
            root.left = deleteNodeV2(root.left, maxLeft.val);
        }
    } else if (root.val > n) {
        root.left = deleteNodeV2(root.left, n);
    } else {
        root.right = deleteNodeV2(root.right, n);
    }
    return root;
}

六、面试官会问什么?🤔

  1. 二叉搜索树的中序遍历有什么特点? 答:中序遍历结果是严格递增的有序序列,这是 BST 最核心的特性,也是判断一棵二叉树是否为 BST 的重要依据。
  2. 为什么二叉搜索树的最坏时间复杂度是 O (n)? 答:当 BST 退化为 "斜树"(所有节点都只有左子树或右子树)时,查找 / 插入 / 删除操作需要遍历所有节点,此时复杂度为 O (n)。因此实际应用中常使用平衡二叉树(如 AVL 树、红黑树)优化。
  3. 删除节点时,为什么要用左子树最大值或右子树最小值替换? 答:因为这两个值是最接近目标节点值的,替换后能保证左子树依然全小于新值、右子树依然全大于新值,维持 BST 的有序性。
  4. 如何验证一棵二叉树是二叉搜索树? 答:可以通过中序遍历,检查结果是否严格递增;也可以递归验证,确保每个节点的左子树最大值小于它,右子树最小值大于它。

七、结语 🌟

二叉搜索树是数据结构中的 "劳模",凭借 "左小右大" 的简单规则,实现了高效的查找、插入和删除操作。掌握它的核心逻辑 ------ 利用有序性减少无效遍历,理解删除操作的三种情况处理,不仅能应对面试,更能在实际开发中灵活运用(比如实现有序映射、索引等)。

希望这篇文章能帮你理清 BST 的脉络,下次遇到相关问题时,也能像 BST 的查找操作一样 "直击要害" 哦!💪

相关推荐
程序员小白条2 小时前
提前实习的好处有哪些?有坏处吗?
java·开发语言·数据结构·数据库·链表
蒙奇D索大2 小时前
【数据结构】排序算法精讲 | 快速排序全解:分治思想、核心步骤与示例演示
数据结构·笔记·学习·考研·算法·排序算法·改行学it
七夜zippoe2 小时前
Python高级数据结构深度解析:从collections模块到内存优化实战
开发语言·数据结构·python·collections·内存视图
wei yun liang2 小时前
4.数据类型
前端·javascript·css3
YGGP2 小时前
【Golang】LeetCode 55. 跳跃游戏
算法·leetcode
练习时长一年4 小时前
Leetcode热题100(跳跃游戏 II)
算法·leetcode·游戏
小白菜又菜9 小时前
Leetcode 3432. Count Partitions with Even Sum Difference
算法·leetcode
wuhen_n10 小时前
LeetCode -- 15. 三数之和(中等)
前端·javascript·算法·leetcode
sin_hielo10 小时前
leetcode 2483
数据结构·算法·leetcode