Java数据结构 二叉搜索树

一、什么是二叉搜索树

二叉搜索树(Binary Search Tree,简称 BST)是一种特殊的二叉树结构。它的核心思想是:通过结点之间的大小关系,把数据组织成一棵可以快速定位的树。只要树保持平衡,查找、插入和删除的效率都会非常高。

理解二叉搜索树,关键要抓住一条贯穿整棵树的有序性规则:左子树 < 根节点 < 右子树。

二、二叉搜索树的性质

二叉搜索树的性质可以归纳为以下三点:

  • 左子树小于根节点:任意结点的左子树中所有结点的值都小于该结点的值。
  • 右子树大于根节点:任意结点的右子树中所有结点的值都大于该结点的值。
  • 递归成立:不仅根节点满足上述关系,它的左右子树本身也各自是一棵二叉搜索树,同样满足这个性质。

也就是说,这条规则不是只对根节点生效,而是对树中的每一个结点都成立。这是二叉搜索树与普通二叉树最本质的区别。

三、二叉搜索树的作用

二叉搜索树最主要的作用是方便查找。由于数据按照大小关系有序组织,查找时不需要遍历整棵树,而是可以像查字典一样不断缩小范围:

  1. 从根节点开始比较目标值。
  2. 如果目标值小于当前结点,就到左子树继续查找。
  3. 如果目标值大于当前结点,就到右子树继续查找。
  4. 如果相等,说明找到了目标结点。

这种"每次排除一半左右"的思路,正是二叉搜索树高效的根本原因。

四、时间复杂度分析

在理想情况下,也就是树比较平衡时,二叉搜索树的查找、插入和删除操作的时间复杂度都是 O(logN)。因为每次比较都能把搜索范围缩小到原来的一半左右,树的高度约为 logN,操作次数与树高成正比。

但需要注意最坏情况:如果插入的数据本身是有序的(例如依次插入 1、2、3、4、5),树就会退化成一条单分支的链状结构,此时树的高度变成 N,查找、插入和删除的时间复杂度都会退化为 O(N)。

小结:二叉搜索树的效率取决于树的高度。平衡时是 O(logN),退化成单分支时是 O(N)。

五、中序遍历得到有序序列

二叉搜索树有一个非常重要的特性:对它进行中序遍历,会得到一个有序的序列。

中序遍历的顺序是"左子树 → 根节点 → 右子树"。由于左子树的所有值都小于根节点,右子树的所有值都大于根节点,按照这个顺序访问,输出的结果自然就是从小到大排列的。

因此,中序遍历常被用来验证一棵二叉树是否为合法的二叉搜索树,也可以用来对树中元素进行排序输出。

六、插入结点的注意事项

在实现二叉搜索树的插入操作时,一般默认不允许插入重复的值。因为二叉搜索树的核心价值在于有序性和唯一性,如果允许重复值,就需要额外约定重复值放在左边还是右边,容易让查找和删除逻辑变得复杂。

常见的处理方式是:插入时如果发现值已存在,则直接忽略该次插入,或者根据业务需要维护一个计数器来记录重复次数。

七、Java 代码实现

下面给出一个完整的 Java 版二叉搜索树实现,包含查找、插入、删除和中序遍历四个核心操作。代码采用迭代方式实现查找与插入,删除操作则单独抽取出 removeNode 方法处理三种情况。为便于阅读,下面把四个核心操作分别拆开讲解。

7.1 查找操作

search 方法从根节点出发,用 cur 指针沿树向下移动:目标值小于当前结点就往左走,大于就往右走,相等则直接返回该结点。循环结束时仍未命中,说明树中不存在该值,返回 null。整个过程只依赖一条从根到叶的路径,因此时间复杂度与树高成正比。

java 复制代码
// 查找
public TreeNode search(int val) {
    if (root == null) {
        return null;
    }
    TreeNode cur = root;
    while (cur != null) {
        if (cur.val > val) {
            cur = cur.left;
        } else if (cur.val == val) {
            return cur;
        } else {
            cur = cur.right;
        }
    }
    return null;
}

7.2 插入操作

insert 方法需要额外维护一个 prev 指针,用来记录 cur 的父结点。循环中一旦发现 cur.val == val,说明值已存在,直接返回 false,这与前面提到的"默认不允许插入重复值"保持一致。循环结束后 cur 为 null,此时根据 prev.val 与待插入值的大小关系,把新结点挂到 prev 的左孩子或右孩子上。

java 复制代码
// 插入
public boolean insert(int val) {
    TreeNode node = new TreeNode(val);
    if (root == null) {
        root = node;
        return true;
    }
    TreeNode cur = root;
    TreeNode prev = null;
    while (cur != null) {
        if (cur.val > val) {
            prev = cur;
            cur = cur.left;
        } else if (cur.val == val) {
            return false;
        } else {
            prev = cur;
            cur = cur.right;
        }
    }
    if (prev.val > val) {
        prev.left = node;
    } else {
        prev.right = node;
    }
    return true;
}

7.3 删除操作

删除是二叉搜索树中最复杂的操作,removeNode 方法按待删除结点的孩子情况分三类处理:

  • 左孩子为空 :直接用右孩子顶替当前结点。若删除的是根节点,则让 root 指向 root.right;否则根据 cur 是父结点的左孩子还是右孩子,把 cur.right 接到对应位置。
  • 右孩子为空:与上一种情况对称,用左孩子顶替当前结点。
  • 左右孩子都不为空 :先到右子树中找最左侧的结点 target,它就是当前结点的中序后继,也是右子树中的最小值。把 target.val 覆盖到 cur.val 上,再把 target 从其父结点上摘除。由于 target 一定没有左孩子,摘除时只需把它的右孩子接到父结点上即可。
java 复制代码
// 删除
public boolean remove(int key) {
    if (root == null) {
        return false;
    }
    TreeNode cur = root;
    TreeNode prev = null;
    while (cur != null) {
        if (cur.val > key) {
            prev = cur;
            cur = cur.left;
        } else if (cur.val == key) {
            removeNode(prev, cur);
            return true;
        } else {
            prev = cur;
            cur = cur.right;
        }
    }
    return false;
}

private void removeNode(TreeNode prev, TreeNode cur) {
    if (cur.left == null) {
        if (cur == root) {
            root = root.right;
            return;
        } else if (prev.right == cur) {
            prev.right = cur.right;
        } else {
            prev.left = cur.right;
        }
    } else if (cur.right == null) {
        if (cur == root) {
            root = root.left;
            return;
        } else if (prev.left == cur) {
            prev.left = cur.left;
        } else {
            prev.right = cur.left;
        }
    } else {
        TreeNode targetParent = cur;
        TreeNode target = cur.right;
        while (target.left != null) {
            targetParent = target;
            target = target.left;
        }
        cur.val = target.val;
        if (targetParent.right == target) {
            targetParent.right = target.right;
        } else {
            targetParent.left = target.right;
        }
    }
}

提示:这里采用"值覆盖 + 删除后继结点"的方式,避免了复杂的指针交换,是面试和工程中都比较常见的写法。

7.4 中序遍历

inOrder 方法是一个标准的递归实现,按照"左子树 → 根节点 → 右子树"的顺序访问。把它作用在二叉搜索树上,输出的序列天然有序,因此常用来验证树的正确性,例如在插入和删除若干元素后调用一次 inOrder(root),观察输出是否严格递增。

java 复制代码
// 中序遍历
public void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    inOrder(root.left);
    System.out.print(root.val + " ");
    inOrder(root.right);
}

7.5 完整类定义

把上面四个方法放回同一个类中,就得到完整的 BinarySearchTree 实现。其中 TreeNode 作为静态内部类定义结点结构,root 保存整棵树的根引用。

java 复制代码
package searchtree;

public class BinarySearchTree {

    public TreeNode root;

    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }
    }

    // 查找、插入、删除、中序遍历的具体实现见上文 7.1 ~ 7.4
}

八、完整测试示例

下面用一个完整的测试类验证前面实现的二叉搜索树。测试依次插入 30、20、40、35、50、38、60,然后删除 40,最后通过中序遍历观察输出结果。这个示例同时覆盖了插入、删除和中序遍历三个操作,比单个方法的零散调用更能说明整棵树的行为。

java 复制代码
public class Test {
    public static void main(String[] args) {
        BinarySearchTree binarySearchTree = new BinarySearchTree();
        binarySearchTree.insert(30);
        binarySearchTree.insert(20);
        binarySearchTree.insert(40);
        binarySearchTree.insert(35);
        binarySearchTree.insert(50);
        binarySearchTree.insert(38);
        binarySearchTree.insert(60);
        binarySearchTree.remove(40);
//        BinarySearchTree.TreeNode search = binarySearchTree.search(50);
//        System.out.println(search.val);
        binarySearchTree.inOrder(binarySearchTree.root);
    }
}

8.1 插入过程分析

按顺序插入 7 个元素后,树的结构如下:

  • 30 作为根节点。
  • 20 小于 30,挂在 30 的左孩子上。
  • 40 大于 30,挂在 30 的右孩子上。
  • 35 小于 40 且大于 30,挂在 40 的左孩子上。
  • 50 大于 40,挂在 40 的右孩子上。
  • 38 小于 40、大于 35,挂在 35 的右孩子上。
  • 60 大于 50,挂在 50 的右孩子上。

此时中序遍历的结果为:20 30 35 38 40 50 60,是一个严格递增的序列,说明树的结构是正确的。

8.2 删除 40 的过程

待删除的结点 40 左右孩子都不为空,属于删除操作中最复杂的一种情况。按照 removeNode 的逻辑,程序会到 40 的右子树中寻找最左侧的结点,也就是右子树的最小值:

  1. 从 40 的右孩子 50 出发,50 有左孩子 35,继续向左。
  2. 35 有右孩子 38,但没有左孩子,因此 35 就是右子树中的最小值,即 40 的中序后继。
  3. 把 35 的值覆盖到 40 所在结点上,此时该结点的值变为 35。
  4. 再把 35 这个后继结点从其父结点 50 上摘除,由于 35 没有左孩子,只需把它的右孩子 38 接到 50 的左孩子位置。

删除完成后,树中不再有 40,中序遍历的结果变为:20 30 35 38 50 60,依然保持有序。

8.3 运行结果

text 复制代码
20 30 35 38 50 60 

可以看到,删除 40 之后,原本位于右子树最左侧的 35 顶替了它的位置,整棵树仍然满足二叉搜索树的性质。如果把代码中被注释掉的两行取消注释,还可以额外验证 search(50) 能正确返回值为 50 的结点。

提示:测试时建议多构造几种删除场景,例如删除叶子结点、只有一个孩子的结点、有两个孩子的结点以及根节点,这样能更全面地验证 removeNode 中各个分支的正确性。

九、总结

  • 二叉搜索树满足:左子树 < 根节点 < 右子树,且对每个结点递归成立。
  • 主要作用是高效查找,查找、插入、删除在平衡情况下为 O(logN)。
  • 最坏情况退化为单分支树,时间复杂度变为 O(N)。
  • 中序遍历二叉搜索树可以得到有序序列。
  • 插入结点时一般不允许插入重复值。
相关推荐
人工智能培训1 小时前
知识图谱:结构化表达实体与关系的知识组织技术
大数据·人工智能·算法·重构·生活·知识图谱
地平线开发者1 小时前
QAT 实战:从 PyTorch 模型到 J6E 实板精度与延时验证
pytorch·算法·自动驾驶
IT星宿1 小时前
github-gravity:用 Rapier2D 把 GitHub 贡献图做成 SVG 物理动画
算法·github
♡すぎ♡2 小时前
SWE 的基本条件
算法·swe·浅水模拟
郝学胜-神的一滴3 小时前
AI 编程智能体 17:四步搞定专属AI智能体
大数据·人工智能·python·神经网络·算法·langchain·强化学习
微尘hjx4 小时前
【StrongSort 03】多轨迹算法StrongSort改进点
人工智能·算法·yolo·视觉检测·ultralytics·strongsort·轨迹算法
梦帮科技4 小时前
QML-04-QGraph-Diff:量子非欧图扩散生成模型与 SE(3) 等变分子晶格逆向合成完整技术指南(上部分)
人工智能·算法·自动化·量子计算·傅里叶分析
七牛开发者4 小时前
TokenGate:端到端可训练的 Token 剪枝机制,让大模型 Prefill 提速 11.3 倍
算法·机器学习·剪枝·长上下文·tokengate