【UCB CS 61B SP24】Lecture 16 - Data Structures 2: ADTs, BSTs学习笔记

news2025/2/27 18:40:09

本文首先介绍了抽象数据类型与树的概念,接着重点讲解二叉搜索树的定义与操作方式,并用 Java 实现一个标准的二叉搜索树结构。

1. 抽象数据类型

首先引入一个概念叫做抽象数据类型(Abstract Data Type,ADT),本课程中我们所实现的各种数据结构其实都设计为了 ADT,那么 ADT 到底是什么概念?它有什么作用?

ADT 是计算机科学中一种重要的设计概念,它通过封装数据结构和操作来隐藏实现细节,仅对外暴露操作接口。在 Java 中,ADT 通过接口(Interface)和类的组合实现。

ADT 只暴露操作(如增删查改),隐藏具体存储方式(如数组、链表或树)。例如:List 接口定义了 add()get(),但不关心底层是 ArrayList(数组)还是 LinkedList(链表)。

这样用户仅需关注“能做什么”,而非“如何实现”。例如:Queueoffer()poll() 方法,无论是用循环数组还是链表实现,调用方式完全一致。

ADT 的核心优势如下:

  • 降低复杂度:用户无需关心底层实现,例如使用 Map 时不必了解哈希表或红黑树的细节。
  • 提高代码复用性:同一接口可适配多种实现,例如 List 可动态切换为数组或链表实现。
  • 增强可维护性:修改底层实现(如优化性能)不会影响外部调用代码。
  • 强制数据完整性:通过封装避免非法操作,例如栈不允许随机访问中间元素。

2. 树

是一种非线性数据结构,由一组节点(Node)和连接这些节点的(Edge)组成,与相对比,树具有以下性质

  • 层级结构:存在唯一的根节点(Root),作为树的起点;
  • 父子关系:除根节点外,每个节点有且仅有一个父节点(Parent),由于树具有层级结构与父子关系,因此可以看作是有方向的,而图的边既可以有方向也可以没方向;
  • 无环性:树中不存在环路(无法从某节点出发沿边回到自身),因此树的边数为节点数减一,而图可以形成环路;
  • 连通性:所有节点通过边连通(全连通),不存在孤立的子结构,而图可以存在孤立子图。

树的核心术语如下:

  • 根节点:树的顶层节点,没有父节点;
  • 子节点:直接连接在当前节点下方的节点;
  • 父节点:直接连接在当前节点上方的节点;
  • 叶子节点:没有子节点的节点(树的末端);
  • 内部节点:至少有一个子节点的节点;
  • 边:连接两个节点的路径;
  • 子树:某个节点及其所有后代组成的子结构;
  • 深度:从根节点到当前节点的路径长度(根节点的深度为0或1,取决于定义);
  • 高度:从当前节点到最远叶子节点的路径长度(叶子节点的高度为0);
  • 度:一个节点拥有的子节点数量。

常见的树有以下这些类型:

  • 二叉树:每个节点最多有两个子节点(左/右);
  • 二叉搜索树:左子树节点值 < 根节点值 < 右子树节点值;
  • 平衡树:通过旋转等操作保持左右子树高度差可控(如 AVL 树、红黑树);
  • 多叉树:每个节点可以有多个子节点(如 B 树、文件系统树);
  • 堆:完全二叉树,满足父节点值 ≥ 或 ≤ 子节点值(最大堆/最小堆)。

树常用链式存储:

class TreeNode<T> {
    T data;
    List<TreeNode<T>> children;  // 多叉树
    TreeNode<T> left, right;  // 二叉树
}

完全二叉树也能用数组存储,父节点索引为 i,则左子节点为 2 * i + 1,右子节点为 2 * i + 2

树具有四种遍历方式:

  • 前序遍历:根 → 左子树 → 右子树,适用于复制树结构;
  • 中序遍历:左子树 → 根 → 右子树,适用于二叉搜索树的有序输出;
  • 后序遍历:左子树 → 右子树 → 根,适用于释放树内存;
  • 层序遍历:按层级从上到下、从左到右,适用于求树的高度/宽度。

树的常见应用场景如下:

  • 文件系统:目录和文件的层级管理;
  • 数据库索引:B/B+ 树加速查询;
  • 游戏 AI:决策树模拟行为逻辑;
  • XML/HTML 解析:DOM 树表示文档结构;
  • 机器学习:决策树分类算法。

3. 二叉搜索树

3.1 基础介绍

假设我们有一个排好序的有序链表如下图所示,如果像查找 G 在哪则需要从头遍历到结尾才能找到,搜索路径为 A -> B -> C -> D -> E -> F -> G

在这里插入图片描述

有什么办法能快点找到这个节点呢?假设我们将链表首部指针移到中间并翻转左半部分的连接,那么搜索时间就会减半!首先我们找到了 D,判断 G 是大于 D 的,那么我们就向右边继续寻找,搜索路径为 D -> E -> F -> G

在这里插入图片描述

既然这样,那么我们继续这样对半分,那么搜索时间就能继续减半,也就构成了二叉搜索树,搜索路径为 D -> F -> G

在这里插入图片描述

二叉搜索树(Binary Search Tree,BST)是一种有序的二叉树数据结构,满足以下性质:

  • 左子树所有节点值小于根节点值;
  • 右子树所有节点值大于根节点值;
  • 左右子树也必须是二叉搜索树。

其核心特性为:

  • 有序性:中序遍历结果为升序序列;
  • 动态操作效率:插入、删除、查找的平均时间复杂度为 O ( l o g n ) O(log n) O(logn),最坏情况为 O ( n ) O(n) O(n)
  • 内存效率:非连续存储,动态扩展;
  • 支持范围查询:可快速找到区间内的所有值。

3.2 操作方法及实现

(1)查找

想查找某个值是否在树中,可以从根节点递归地往下找,一共会有以下几种情况:

  • 当前节点为 null,则已经查完整棵树了还未找到,不存在目标值,返回 false
  • 当前节点值 = 目标值,已找到,返回 true
  • 当前节点值 > 目标值,说明要查找的目标值在子树上,递归查找子树;
  • 当前节点值 < 目标值,说明要查找的目标值在子树上,递归查找子树。

(2)插入

插入新值时同样也是采用递归的思想从根节点开始寻找插入的位置,流程不难理解:

  • 若树为空,新节点直接成为根节点;
  • 若树非空,则比较新值与当前节点值,有以下几种情况:
    • 新值 = 当前节点值:根据需求处理(通常不做处理,也就是不插入重复值);
    • 新值 < 当前节点值:向子树递归插入;
    • 新值 > 当前节点值:向子树递归插入;
    • 当前节点为 null:找到了插入位置,创建新节点并返回。

(3)删除

删除操作相对复杂一点,我们可以将其分为三种情况来讨论:

  • 删除叶节点:直接删除即可,即将其父节点所指向它的指针置为 null

  • 删除有一个子节点(或一边子树)的节点:将其父节点直接连接到该节点的子节点上,为什么这样构建新指针一定是对的?因为假如该节点在父节点的左侧,那么该节点及其子树一定也小于其父节点,因此父节点指向该节点的子树作为左子树一定是合理的,同理假如在右侧也一样。例如我们删除下面这棵树的 "flat"
    在这里插入图片描述
    在这里插入图片描述

  • 删除有两个子节点的节点:这种情况最复杂,我们需要选出一个节点顶替到当前节点的位置上,也就是将所选节点的值覆盖到当前节点后再将所选节点删除。那么选哪个节点最方便?答案是右子树中的最小节点(或左子树中的最大节点)。因为这两种类型的节点替换到当前节点上都能保证当前节点左子树中的所有值都小于自身,且右子树中的所有值的都大于自身,这两种类型的节点一定满足没有子树或最多只有一棵子树。例如我们删除下面这棵树的 k,最适合替代 k 的值为 gm
    在这里插入图片描述
    我们选择用 g 替代 k,然后将 g 删除(回到了第二种情况,直接将 e 指向 f 即可完成 g 的删除):
    在这里插入图片描述

3.3 Java实现BinarySearchTree

根据前面的讲解,我们就可以实现二叉搜索树的每个操作:

package CS61B.Lecture16;

public class BinarySearchTree<T extends Comparable<T>> {
    private class Node {
        private T val;
        private Node left;
        private Node right;

        public Node(T val) {
            this.val = val;
            left = null;
            right = null;
        }
    }

    private Node root;

    public BinarySearchTree() {
        root = null;
    }

    public void insert(T val) {
        root = insertRecursive(root, val);
    }

    private Node insertRecursive(Node node, T val) {
        if (node == null) return new Node(val);  // 已找到插入位置

        int cmp = val.compareTo(node.val);
        if (cmp < 0) node.left = insertRecursive(node.left, val);  // 往左子树递归寻找插入位置
        else if (cmp > 0) node.right = insertRecursive(node.right, val);  // 往右子树递归寻找插入位置
        // 若值已存在则不做处理

        return node;
    }

    public boolean search(T val) {
        return searchRecursive(root, val);
    }

    private boolean searchRecursive(Node node, T val) {
        if (node == null) return false;  // 未找到目标值

        int cmp = val.compareTo(node.val);
        if (cmp < 0) return searchRecursive(node.left, val);  // 往左子树递归查找
        else if (cmp > 0) return searchRecursive(node.right, val);  // 往右子树递归查找
        else return true;  // 当前值等于目标值,已找到
    }

    public void delete(T val) {
        root = deleteRecursive(root, val);
    }

    private Node deleteRecursive(Node node, T val) {
        if (node == null) return null;

        int cmp = val.compareTo(node.val);
        if (cmp < 0) node.left = deleteRecursive(node.left, val);
        else if (cmp > 0) node.right = deleteRecursive(node.right, val);
        else {  // 已找到要删除的目标值
            // Case 1 & 2:没有子树或只有一棵子树
            if (node.left == null) return node.right;  // 如果没有子树则返回 null,如果有右子树则返回右子树
            if (node.right == null) return node.left;  // 如果有左子树则返回左子树

            // Case 3:既有左子树也有右子树
            Node minNodeInRight = findMinNodeInRight(node.right);  // 找出右子树中的最小值
            node.val = minNodeInRight.val;  // 替换到当前节点上
            node.right = deleteRecursive(node.right, minNodeInRight.val);  // 递归地在右子树中删除这个最小值
        }

        return node;
    }

    private Node findMinNodeInRight(Node node) {
        while (node.left != null) node = node.left;
        return node;
    }

    // 中序遍历,用于验证二叉搜索树是否正确
    public void inOrderTraversal() {
        inOrderTraversalRecursive(root);
        System.out.println();
    }

    private void inOrderTraversalRecursive(Node node) {
        if (node == null) return;

        inOrderTraversalRecursive(node.left);  // 先递归遍历左子树
        System.out.print(node.val + " ");  // 输出当前节点的值
        inOrderTraversalRecursive(node.right);  // 再递归遍历右子树
    }

    public static void main(String[] args) {
        BinarySearchTree<Integer> bst = new BinarySearchTree<>();

        bst.insert(50);
        bst.insert(30);
        bst.insert(20);
        bst.insert(40);
        bst.insert(70);
        bst.insert(60);
        bst.insert(80);
        bst.inOrderTraversal();  // 20 30 40 50 60 70 80

        System.out.println(bst.search(10));  // false
        System.out.println(bst.search(20));  // true

        bst.delete(50);
        bst.delete(20);
        bst.delete(30);
        bst.inOrderTraversal();  // 40 60 70 80
    }
}

注意代码中的 T extends Comparable<T> 这是表示强制泛型类型 T 实现比较逻辑,确保二叉搜索树的核心操作(插入、查找、删除)可以正确执行,因为 BST 中需要对值进行比较:val.compareTo(node.val)

假设我们定义一个 Person 类,并尝试用 BinarySearchTree<Person> 存储,只要我们这个类是可比较的,那么就可以正常使用 BST:

class Person implements Comparable<Person> {
    String name;
    int age;

    @Override
    public int compareTo(Person other) {
        return this.age - other.age;  // 按年龄比较
    }
}

// 合法:Person 实现了 Comparable<Person> 接口,并重写了 compareTo 方法,具有合法的比较逻辑
BinarySearchTree<Person> bst = new BinarySearchTree<>();
bst.insert(new Person("Alice", 18));

3.4 二叉搜索树与集合

我们回顾一下集合的特性:

  • 不允许重复元素。
  • 支持高效插入、删除和查找操作。
  • 可以有序(如 TreeSet)或无序(如 HashSet)。

这么一看我们的二叉搜索树是不是就是类似集合的数据结构?二叉搜索树确实可以用来实现集合,但它的应用不仅限于此。

利用 BST 的有序性动态操作特性,可以实现一个有序集合。例如 Java 的 TreeSet 底层通过红黑树(一种平衡二叉搜索树)实现,保证了插入、删除、查找的时间复杂度为 O ( l o g n ) O(log n) O(logn)

普通 BST 存在退化为链表的可能(例如从小到大插入元素),即时间复杂度退化为 O ( n ) O(n) O(n),因此实际应用中通常使用平衡二叉搜索树(如 AVL 树、红黑树),这些数据结构在后面马上就会讲到。

以下是一些直接或间接基于 BST 的经典数据结构:

数据结构核心思想应用场景
TreeSet基于红黑树实现的有序集合需要维护元素顺序的集合操作
TreeMap基于红黑树实现的有序键值对映射字典、范围查询
AVL 树严格平衡的 BST,通过旋转保持左右子树高度差 ≤ 1对查询效率要求极高的场景
红黑树近似平衡的 BST,通过颜色标记和旋转降低平衡成本Java 的 TreeMap/TreeSet
B 树/B+ 树多路平衡搜索树(每个节点可存多个值),是 BST 的扩展数据库索引、文件系统

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 大视界 -- Java 大数据在智能物流路径规划与车辆调度中的创新应用(102)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

HarmonyOS Design 介绍

HarmonyOS Design 介绍 文章目录 HarmonyOS Design 介绍一、HarmonyOS Design 是什么&#xff1f;1. 设计系统&#xff08;Design System&#xff09;2. UI 框架的支持3. 设计工具和资源4. 开发指南5. 与其他设计系统的对比总结 二、HarmonyOS Design 特点 | 应用场景1. Harmon…

【算法系列】快速排序详解

文章目录 快速排序的多种实现方式1. 基本快速排序&#xff08;Lomuto 分区方案&#xff09;1.1 基本原理1.2 步骤1.3 Java 实现示例 2. Hoare 分区方案2.1 基本原理2.2 步骤2.3 Java 实现示例 3. 三数取中法3.1 基本原理3.2 步骤3.3 Java 实现示例 4. 尾递归优化4.1 基本原理4.…

电脑键盘知识

1、键盘四大功能区 1. 功能区 2. 主要信息输入区 3. 编辑区 4. 数字键盘区 笔记本电脑键盘的功能区&#xff0c;使用前需先按Fn键 1.1、功能区 ESC&#xff1a;退出 F1&#xff1a;显示帮助信息 F2&#xff1a;重命名 F4&#xff1a;重复上一步操作 F5&#xff1a;刷新网页 …

Grok 3 vs. DeepSeek vs. ChatGPT:2025终极AI对决

2025 年,AI 领域的竞争愈发激烈,三个重量级选手争夺霸主地位:Grok 3(由 xAI 开发)、DeepSeek(国内 AI 初创公司)和 ChatGPT(OpenAI 产品)。每个模型都有自己独特的优势,无论是在深度思考、速度、编程辅助、创意输出,还是在成本控制方面,都展现出强大的实力。但究竟…

【MySQL篇】数据库基础

目录 1&#xff0c;什么是数据库&#xff1f; 2&#xff0c;主流数据库 3&#xff0c;MySQL介绍 1&#xff0c;MySQL架构 2&#xff0c;SQL分类 3&#xff0c;MySQL存储引擎 1&#xff0c;什么是数据库&#xff1f; 数据库&#xff08;Database&#xff0c;简称DB&#xf…

基于SpringBoot+mybatisplus+vueJS的Cosplay文化展示与交流社区设计与实现

博主介绍&#xff1a;硕士研究生&#xff0c;专注于信息化技术领域开发与管理&#xff0c;会使用java、标准c/c等开发语言&#xff0c;以及毕业项目实战✌ 从事基于java BS架构、CS架构、c/c 编程工作近16年&#xff0c;拥有近12年的管理工作经验&#xff0c;拥有较丰富的技术架…

组件传递props校验

注意&#xff1a;prop是只读的&#xff01;不可以修改父组件的数据。 可以检验传过来的内容是否类型没问题。 App.vue <template><div><!-- <parentDemo/> --><componentA/></div></template> <script> import ComponentA …

数据结构与算法-图论-最短路-拓展运用

选择最佳路线 分析&#xff1a; 这是一道图论中的最短路径问题&#xff0c;目标是在给定的公交网络中&#xff0c;找到从琪琪家附近的车站出发&#xff0c;到她朋友家附近车站&#xff08;编号为 s &#xff09;的最短时间。以下是对该问题的详细分析&#xff1a; 问题关键信息…

0—QT ui界面一览

2025.2.26&#xff0c;感谢gpt4 1.控件盒子 1. Layouts&#xff08;布局&#xff09; 布局控件用于组织界面上的控件&#xff0c;确保它们的位置和排列方式合理。 Vertical Layout&#xff08;垂直布局&#xff09; &#xff1a;将控件按垂直方向排列。 建议&#xff1a;适…

纷析云:赋能企业财务数字化转型的开源解决方案

在企业数字化转型的浪潮中&#xff0c;财务管理的高效与安全成为关键。纷析云凭借其开源、安全、灵活的财务软件解决方案&#xff0c;为企业提供了一条理想的转型路径。 一、开源的力量&#xff1a;自主、安全、高效 纷析云的核心优势在于其100%开源的财务软件源码。这意味着…

P8716 [蓝桥杯 2020 省 AB2] 回文日期

1 题目说明 2 题目分析 暴力不会超时&#xff0c;O(n)的时间复杂度&#xff0c; < 1 0 8 <10^8 <108。分析见代码&#xff1a; #include<iostream> #include<string> using namespace std;int m[13]{0,31,28,31,30,31,30,31,31,30,31,30,31};// 判断日期…

(十)趣学设计模式 之 外观模式!

目录 一、 啥是外观模式&#xff1f;二、 为什么要用外观模式&#xff1f;三、 外观模式的实现方式四、 外观模式的优缺点五、 外观模式的应用场景六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xff0c;可以多多支…

apache-maven-3.2.1

MAVEN_HOME D:\apache-maven-3.2.1 PATH D:\apache-maven-3.2.1\bin cmd mvn -v <localRepository>d:\localRepository</localRepository> setting.xml <?xml version"1.0" encoding"UTF-8"?><!-- Licensed to the Apache Soft…

编程题-连接两字母单词得到的最长回文串(中等)

题目&#xff1a; 给你一个字符串数组 words 。words 中每个元素都是一个包含 两个 小写英文字母的单词。 请你从 words 中选择一些元素并按 任意顺序 连接它们&#xff0c;并得到一个 尽可能长的回文串 。每个元素 至多 只能使用一次。 请你返回你能得到的最长回文串的 长度…

论文笔记(七十二)Reward Centering(三)

Reward Centering&#xff08;三&#xff09; 文章概括摘要3 基于值的奖励中心化4 案例研究&#xff1a; 以奖励为中心的 Q-learning5 讨论、局限性与未来工作致谢 文章概括 引用&#xff1a; article{naik2024reward,title{Reward Centering},author{Naik, Abhishek and Wan…

【论文笔记-ECCV 2024】AnyControl:使用文本到图像生成的多功能控件创建您的艺术作品

AnyControl&#xff1a;使用文本到图像生成的多功能控件创建您的艺术作品 图1 AnyControl的多控制图像合成。该研究的模型支持多个控制信号的自由组合&#xff0c;并生成与每个输入对齐的和谐结果。输入到模型中的输入控制信号以组合图像显示&#xff0c;以实现更好的可视化。 …

Redisson使用场景及原理

目录 一、前言 二、安装Redis 1、Windows安装Redis ​2、启动方式 3、设置密码 三、项目集成Redission客户端 1、引入依赖 四、实用场景 1、操作缓存 2、分布式锁 3、限流 3.1 创建限流器 3.2 设置限流参数 3.3 获取令牌 3.4 带超时时间获取令牌 3.5 总结 一、…

【二分查找 图论】P8794 [蓝桥杯 2022 国 A] 环境治理|普及

本文涉及的基础知识点 本博文代码打包下载 C二分查找 C图论 [蓝桥杯 2022 国 A] 环境治理 题目描述 LQ 国拥有 n n n 个城市&#xff0c;从 0 0 0 到 n − 1 n - 1 n−1 编号&#xff0c;这 n n n 个城市两两之间都有且仅有一条双向道路连接&#xff0c;这意味着任意两…

25中医研究生复试面试问题汇总 中医专业知识问题很全! 中医试全流程攻略 中医考研复试调剂真题汇总

各位备考中医研究生的小伙伴们&#xff0c;一想到复试&#xff0c;是不是立刻紧张到不行&#xff0c;担心老师会抛出一大堆刁钻的问题&#xff1f;别怕&#xff01;其实中医复试也是有套路可循的&#xff0c;只要看完这篇攻略&#xff0c;你就会发现复试并没有想象中那么难&…