目录

二叉搜索树的数据结构

手写实现二叉搜索树

树节点定义 

插入节点 

源码 

流程图 

 二叉树插入步骤图解

第一步: 插入 20 

第二步: 插入 10

第三步: 插入 30

第四步: 插入 5

查找节点 

源码 

场景一: 查找成功 (search for 25)

第一步: 从根节点开始

第二步: 移动到右子树

第三步: 找到目标

场景二: 查找失败 (search for 12) 

第一步: 从根节点开始

第二步: 移动到左子树

第三步: 继续向右查找

删除节点 

源码 

核心删除逻辑: delete(Node delNode)

BST 删除节点实例图解 

场景一: 删除叶子节点 (Delete 5) 

场景二: 删除只有一个孩子的节点 (Delete 15)

场景三: 删除有两个孩子的节点 (Delete 20)

第 1 步: 识别目标 (delNode) 与后继 (miniNode)

第 2 步: 处理后继节点的子树 (第一次 transplant) 

第 3 步: 连接后继节点与待删节点的右子树

第 4 步: 将后继节点换到待删位置 (第二次 transplant)

第 5 步: 连接后继节点与待删节点的左子树 (最终状态)

常见问题 

二叉搜索树结构简述&变T的可能也让手写

二叉搜索树的插入、删除、索引的时间复杂度 

二叉搜索树删除含有双子节点的元素过程叙述 

二叉搜索树的节点都包括了哪些信息

为什么Java HashMap 中说过红黑树而不使用二叉搜索树  


二叉搜索树的数据结构

二叉搜索树(Binary Search Tree),也称二叉查找树。如果你看见有序二叉树(Ordered Binary tree)、排序二叉树(Sorted Binary Tree)那么说的都是一个东西。

  • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也分别为二叉查找树; 

手写实现二叉搜索树
 

树节点定义 

public class Node {public Class<?> clazz;public Integer value;public Node parent;public Node left;public Node right;public Node(Class<?> clazz, Integer value, Node parent, Node left, Node right) {this.clazz = clazz;this.value = value;this.parent = parent;this.left = left;this.right = right;}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((value == null) ? 0 : value.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (null == obj) return false;if (getClass() != obj.getClass()) return false;Node node = (Node) obj;if (null == value) {return node.value == null;} else {return node.value.equals(value);}}
}

插入节点 

源码 

public Node insert(int e) {if (null == root) {root = new Node(e, null, null, null);size++;return root;}// 索引出待插入元素位置,也就是插入到哪个父元素下Node parent = root;Node search = root;while (search != null && search.value != null) {parent = search;if (e < search.value) {search = search.left;} else {search = search.right;}}// 插入元素Node newNode = new Node(e, parent, null, null);if (parent.value > newNode.value) {parent.left = newNode;} else {parent.right = newNode;}size++;return newNode;
}

流程图 

 二叉树插入步骤图解

以一个简单的例子,依次插入数组 [20, 10, 30, 5] 中的四个数字,并画出每一步操作后树的结构变化。

第一步: 插入 20 

执行操作: 调用 insert(20)。
代码路径: 此时树是空的,root 为 null。代码执行第一个 if 分支,创建一个新的根节点,其值为20。

image.png

第二步: 插入 10

执行操作: 调用 insert(10)。
代码路径: root (20) 不为 null。进入 while 循环:

  • 将 10 与根节点 20 比较,因为 10 < 20,所以 search 指针移动到左子节点 (当前为 null)。
  • search 变为 null,循环结束。此时的 parent 仍然是节点 20。
  • 创建一个值为 10 的新节点,并将其作为 parent (节点20) 的左孩子。 

第三步: 插入 30

执行操作: 调用 insert(30)。
代码路径: root (20) 不为 null。进入 while 循环:

  • 将 30 与根节点 20 比较,因为 30 > 20,所以 search 指针移动到右子节点 (当前为 null)。
  • search 变为 null,循环结束。此时的 parent 仍然是节点 20。
  • 创建一个值为 30 的新节点,并将其作为 parent (节点20) 的右孩子

第四步: 插入 5

执行操作: 调用 insert(5)。
代码路径: root (20) 不为 null。进入 while 循环:

  • 第1轮循环: 5 < 20,search 移动到左孩子 (节点10)。parent 更新为节点 20。
  • 第2轮循环: 5 < 10,search 移动到左孩子 (当前为 null)。parent 更新为节点 10。
  • search 变为 null,循环结束。此时的 parent 是节点 10。
  • 创建一个值为 5 的新节点,并将其作为 parent (节点10) 的左孩子。 

查找节点 

源码 

public Node search(int e) {Node node = root;while (node != null && node.value != null && node.value != e) {if (e < node.value) {node = node.left;} else {node = node.right;}}return node;
}

初始树结构
假设我们的树通过依次插入 `[20, 10, 30, 5, 15, 25, 35]` 构建而成。

image.png

场景一: 查找成功 (search for 25)

我们来查找一个存在于树中的值:`25`。 

第一步: 从根节点开始
  • 当前节点 node: 20

  • 比较: `25 < 20` 为假。

  • 操作: 进入 `else` 分支,将 node 更新为其右孩子。node = node.right。

第二步: 移动到右子树
  • 当前节点 node: 30

  • 比较: `25 < 30` 为真。
  • 操作: 进入 `if` 分支,将 node 更新为其左孩子。node = node.left。

image.png

第三步: 找到目标
  • 当前节点 node: 25

  • 循环条件检查: `node.value != e` (即 `25 != 25`) 为假。
  • 操作: 循环条件不满足,退出 while 循环。 

结果: 方法返回值为 25 的 Node 对象。 

场景二: 查找失败 (search for 12) 

现在,我们查找一个不存在于树中的值:`12`。

第一步: 从根节点开始
  • 当前节点 node: 20
  • 比较: `12 < 20` 为真,node 移动到左孩子 (10)。
第二步: 移动到左子树
  • 当前节点 node: 10
  • 比较: `12 < 10` 为假。
  • 操作: 进入 `else` 分支,node 更新为其右孩子。node = node.right。

第三步: 继续向右查找
  • 当前节点 node: 15
  • 比较: `12 < 15` 为真。
  • 操作: 进入 `if` 分支,node 更新为其左孩子。

 第四步: 到达叶节点末端

  • 当前节点 node: `null` (因为节点 15 没有左孩子)。
  • 循环条件检查: `node != null` 为假。
  • 操作: 循环条件不满足,退出 while 循环。

树的结构保持不变,但我们的 node 指针现在是 null。

结果: 方法返回 null,表示未找到值为 12 的节点。 

删除节点 

源码 

public Node delete(int e) {Node delNode = search(e);if (null == delNode) return null;return delete(delNode);
}private Node delete(Node delNode) {if (delNode == null) return null;Node result = null;if (delNode.left == null) {result = transplant(delNode, delNode.right);} else if (delNode.right == null) {result = transplant(delNode, delNode.left);} else {// 因为删除的节点,有2个孩子节点,这个时候找到这条分支下,最左侧做小的节点。用它来替换删除的节点Node miniNode = getMiniNode(delNode.right);if (miniNode.parent != delNode) {// 交换位置,用miniNode右节点,替换miniNodetransplant(miniNode, miniNode.right);// 把miniNode 提升父节点,设置右子树并进行挂链。替代待删节点miniNode.right = delNode.right;miniNode.right.parent = miniNode;}// 交换位置,删除节点和miniNode 可打印测试观察;System.out.println(this);transplant(delNode, miniNode);// 把miniNode 提升到父节点,设置左子树并挂链miniNode.left = delNode.left;miniNode.left.parent = miniNode;result = miniNode;}size--;return result;
}
private Node getMinimum(Node node) {while (node.left != null) {node = node.left;}return node;
}private Node transplant(Node delNode, Node addNode) {if (delNode.parent == null) {this.root = addNode;}// 判断删除元素是左/右节点else if (delNode.parent.left == delNode) {delNode.parent.left = addNode;} else {delNode.parent.right = addNode;}// 设置父节点if (null != addNode) {addNode.parent = delNode.parent;}return addNode;
}

核心删除逻辑: delete(Node delNode)

这是处理删除操作的核心方法。它分为三个主要分支:

  1. 情况1: 要删除的节点没有左孩子。

  2. 情况2: 要删除的节点有左孩子,但没有右孩子。
  3. 情况3: 要删除的节点有两个孩子(最复杂的情况)。 

transplant 方法是实现删除的核心。它的作用是在树中用一个节点 (addNode) 替换另一个节点 (delNode),并正确地维护父节点的链接关系。它本身并不处理子节点的链接,这由 delete 方法完成。
getMinimum 是一个简单的辅助方法,用于在给定的子树中找到值最小的节点。它通过不断地沿着左子节点向下遍历直到末端来实现。

BST 删除节点实例图解 

我们将使用一个具体的二叉搜索树来逐步演示删除操作的三个场景。在图示中:

  1. 红色节点: 将要被删除的目标节点 (delNode)。
  2. 黄色节点: 用于替换的后继节点 (miniNode)。
  3. 紫色边: 表示正在发生改变的父子链接。 

初始树结构
我们的示例树通过依次插入 `[20, 10, 30, 5, 15, 25, 35]` 构建而成。

image.png

场景一: 删除叶子节点 (Delete 5) 

执行步骤

  1. delete(5) 调用 search(5) 找到节点 5。
  2. 进入 delete(Node delNode) 方法,delNode 是节点 5。
  3. 检查 delNode.left == null 为真。
  4. 调用 transplant(delNode, delNode.right),即 transplant(5, null)。
  5. 在 transplant 中,delNode 的父节点 (10) 的左孩子 (left) 被设置为 null。
  6. 删除完成。

image.png

场景二: 删除只有一个孩子的节点 (Delete 15)

注: 为演示此场景,我们创建一棵新树 `[20, 10, 30, 5, 15, 35]`,其中节点 15 只有一个左孩子 5。我们会用它的子节点替换它。 

image.png

image.png

执行逻辑: transplant(15, 5) 被调用,节点 10 的右孩子指针直接指向了节点 5,同时更新了节点 5 的父指针。 

场景三: 删除有两个孩子的节点 (Delete 20)

这是最复杂的情况,我们将删除根节点 20。这会触发代码中最完整的逻辑:寻找后继节点 (右子树的最小值),并用它来替换被删除的节点。

第 1 步: 识别目标 (delNode) 与后继 (miniNode)

我们想删除 20。因为它有两个孩子,我们必须找到它的后继节点。通过 `getMinimum(delNode.right)`,我们找到其右子树 (以30为根) 的最小值,即 25。 

第 2 步: 处理后继节点的子树 (第一次 transplant) 

代码检查 `miniNode.parent != delNode` (即 30 != 20),这个条件为真。因此,我们需要先将 `miniNode` (25) 提拔上来。我们调用 transplant(miniNode, miniNode.right),即 transplant(25, null),将 25 原来的父节点 (30) 的左孩子指向 25 的右孩子 (null)。

第 3 步: 连接后继节点与待删节点的右子树

执行 miniNode.right = delNode.right。我们将 20 的右子树 (以 30 为根) 变成 25 的右子树。

image.png

第 4 步: 将后继节点换到待删位置 (第二次 transplant)

调用 transplant(delNode, miniNode),即 transplant(20, 25)。因为 20 是根节点,这会将树的 root 指针更新为 25。

image.png

第 5 步: 连接后继节点与待删节点的左子树 (最终状态)

最后,执行 miniNode.left = delNode.left,将 20 的左子树 (以 10 为根) 挂到 25 的左边,完成整个删除操作。 

常见问题 

二叉搜索树结构简述&变T的可能也让手写

某个节点的左子树所有值都小于该节点,右子树中所有值都大于该节点 

二叉搜索树的插入、删除、索引的时间复杂度 

插入、删除、查找的时间复杂度取决于树的高度

理想情况下是 O(log n), 但最坏情况下会退化为 O(n) 

二叉搜索树删除含有双子节点的元素过程叙述 

删除一个有两个子节点的节点时,不能直接删除掉它,而是从它的右子树中找一个最小的节点(刚好比他大一点点),用这个节点来顶替他的位置。
例如:删除节点 delNode 他有两个子节点 delNode.left、delNode.right

  1. 从它的右子树中找一个合适的节点来替代它 addNode
  2. 因为要用 addNode 节点来替换 delNode,所以需要先把 addNode 原来的位置清理干净,最多只有一个右孩子(没有左孩子),用右孩子来替换 addNode的位置
  3. 把delNode的父节点 指向 addNode 、把delNode的左右子树接到 addNode上面 

二叉搜索树的节点都包括了哪些信息

Integer value 值;
Node parent 父节点;
Node left 左子节点;
Node right 右子节点;

为什么Java HashMap 中说过红黑树而不使用二叉搜索树  

是为了避免 BST 在极端情况下退化为链表,从而保证查找、插入和删除操作始终保持 O(log n) 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93269.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93269.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/93269.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

四、计算机组成原理——第1章:计算机系统概述

目录 1.1计算机发展历程 1.1.1计算机硬件的发展 1.计算机的四代变化 2.计算机元件的更新换代 1.1.2计算机软件的发展 1.2计算机系统层次结构 1.2.1计算机系统的组成 1.2.2计算机硬件 1.冯诺依曼机基本思想 2.计算机的功能部件 (1)输入设备 (2)输出设备 (3)存储器 (4)运算器 (5)…

flutter TextField 失去焦点事件

在 Flutter 中&#xff0c;处理 TextField 的失去焦点事件&#xff08;即失去焦点时触发的操作&#xff09;通常有两种常用方式&#xff1a;使用 FocusNode 或 onEditingComplete 回调。以下是具体实现&#xff1a; import package:flutter/material.dart;class MyTextField e…

Moonlight for ChromeOS 常见问题解决方案

Moonlight for ChromeOS 常见问题解决方案 项目基础介绍 Moonlight for ChromeOS 是一个开源的 NVIDIA GameStream 客户端&#xff0c;允许用户将他们的游戏从高性能的桌面电脑流式传输到运行 ChromeOS 的设备上。该项目还支持 Android 和 iOS/tvOS 平台。Moonlight for Chrome…

SQL语句:读操作、写操作、视图

文章目录读操作分类基础查询语句示例高级查询--分组查询、子查询、表连接、联合查询分组查询&#xff1a;子查询&#xff08;嵌套查询&#xff09;表连接联合查询写操作视图SQL&#xff1a;结构化查询语言读操作 重点是where查询&#xff0c;即高级查询部分 分类 DML &#…

Python 机器学习实战:基于 Scikit-learn

本文围绕《Python 机器学习实战&#xff1a;基于 Scikit-learn 的项目开发》展开&#xff0c;先介绍 Scikit-learn 库的基础特性与优势&#xff0c;再阐述机器学习项目开发的完整流程&#xff0c;包括数据收集与预处理、模型选择与训练、评估与优化等。通过具体实战案例&#x…

java里List链式编程

java里对list的操作&#xff0c;我们一遍使用for遍历&#xff0c;输出或改变里面的内容。单经常在代码里面我们发现&#xff0c;也可以使用这样的代码结构daPaymentActionVo.setApnolist(paymentActionVo.getApnolist().stream().map(PaymentActionVo.Voucher::getApno).collec…

【esp32s3】7 - VSCode + PlatformIO + Arduino + 构建项目

一、PlatformIO 1.1. 概述 官方文档&#xff1a;What is PlatformIO? PlatformIO 是一个跨平台的物联网开发生态系统&#xff0c;专门为嵌入式系统开发设计&#xff0c;支持多种开发板和框架。 1.1.1. 主要特点 跨平台&#xff1a;支持 Windows、macOS 和 Linux多框架支持&…

LE AUDIO CIS/BIS音频传输时延的计算

LE AUDIO音频总时延计算方法 按照BAP的规范,LE AUDIO音频总延时包括三个部分:Audio Processing Time,Transport Latency,Presentation Delay。如下图所示是播放音乐的示例图: 这里还有一个麦克风录音的总时延示例图: Audio Processing Time:这个就是音频DSP获取音频数…

git 修改 更新

git 修改 更新先更新&#xff0c;后修改# 暂存当前修改 git add . git stash# 获取最新的 main 分支 git checkout main git pull# 新建开发分支 git checkout -b lbg_0727# ⚠️ 先把 main 的最新代码合并/变基到当前分支&#xff08;用于消除冲突&#xff09; # 方法1&#x…

飞鹤困局:增长神话的裂痕

增长天花板已然逼近&#xff0c;飞鹤需要探寻新方向。作者|安德鲁编辑|文昌龙“飞鹤&#xff0c;更适合中国宝宝体质”——这句曾让无数妈妈点头的广告语&#xff0c;帮飞鹤坐上了中国奶粉市场的头把交椅。可多年后&#xff0c;时代红利退潮&#xff0c;故事不好讲了。飞鹤的利…

Java设计模式之<建造者模式>

目录 1、建造者模式 2、建造者模式结构 3、实现 4、工厂模式对比 5、适用场景差异 前言 建造者模式是一种创建型设计模式。用于封装复杂对象的构建过程&#xff0c;通过步骤构建产品类。它包括产品类、抽象建造者、具体建造者和指挥者角色。 优点在于灵活性、解耦和易扩展…

fchown/fchownat系统调用及示例

55. fchmod - 通过文件描述符改变文件权限 函数介绍 fchmod是一个Linux系统调用&#xff0c;用于通过文件描述符来改变文件的访问权限。它是chmod函数的文件描述符版本&#xff0c;避免了路径名解析。 函数原型 #include <sys/stat.h> #include <unistd.h>int fchm…

20250726-5-Kubernetes 网络-Service 代理模式详解(iptables与ipvs)_笔记

一、服务三种常用类型  1. LoadBalancer类型 工作原理:与NodePort类似,在每个节点上启用端口暴露服务,同时Kubernetes会请求底层云平台(如阿里云、腾讯云、AWS等)的负载均衡器,将每个Node([NodeIP]:[NodePort])作为后端添加。 自动化实现:云厂商通过官方实现的控制…

horizon置备出错

报错内容如下&#xff1a; [2025/7/28 19:15] 置备 Customization failure: Customization of the guest operating system is not supported due to the given reason: 期间出错 解决方法&#xff1a;将模板转换为虚拟机&#xff0c;安装vmtools&#xff1b;再安装vmtools之后…

【unitrix】 6.19 Ord特质(ord.rs)

一、源码 这段代码定义了一个标记特征&#xff08;marker trait&#xff09;Ord 和三个实现&#xff0c;用于将类型标记与 Rust 标准库中的 Ordering 枚举关联起来。 use crate::sealed::Sealed; use core::cmp::Ordering; use crate::number::{Greater, Equal, Less}; /// 用于…

数据结构之顺序表链表栈

顺序表 什么是 list list 的使用 线性表是什么 顺序表是什么 顺序表和线性表的关系 顺序表和数组的区别 List 和 ArrayList 的关系 如何自己模拟实现 myArrayList ArrayList 的构造 ArrayList 的常见方法 以下两种写法有什么区别 ArrayList<Integer> arrayLis…

day062-监控告警方式与Grafana优雅展示

文章目录0. 老男孩思想-马太效应1. API监控2. zabbix的API接口2.1 生成zabbix的api token2.2 访问格式2.3 前端添加web监测3. 监控告警方式3.1 云监控-邮件告警3.1.1 邮箱开启授权码3.1.2 zabbix前端配置3.1.3 消息模板3.1.4 配置邮箱收件人信息3.1.5 配置触发器3.2 企业微信告…

Ettus USRP X410/X440 运行 ADC 自校准

Ettus USRP X410/X440 运行 ADC 自校准 打开一个接收&#xff08;Rx&#xff09;会话到您在设备名称输入中指定的设备并返回会话句柄 out&#xff0c;您可以使用该句柄在所有后续 NI-USRP VI 中识别此仪器会话。 支持设备&#xff1a;Ettus USRP X410/X440输入/输出 文明.png 会…

Qt元类型系统(QMetaType)详解

Qt元类型系统详解一、Qt元类型系统(QMetaType)详解1. 核心功能2. 注册机制3. 关键技术点4. 信号槽支持5. 流式传输支持6. 使用场景7. 注意事项二、完整示例1、基本实例2、基本实例3、元类型在信号槽中的应用4、高级用法三、元对象编译器moc元对象编译器&#xff08;Moc&#xf…

《C++继承详解:从入门到理解公有、私有与保护继承》

《C继承详解&#xff1a;从入门到理解公有、私有与保护继承》 文章目录《C继承详解&#xff1a;从入门到理解公有、私有与保护继承》一、继承的概念及定义1.1 继承的概念1.2 继承定义1.2.1 定义格式1.2.2 继承基类成员访问方式的变化1.3 继承类模版二、基类和派生类间的转换三、…