【UCB CS 61B SP24】Lecture 5 - Lists 3: DLLists and Arrays学习笔记
本文内容为构建双向循环链表、使用 Java 的泛型将其优化为通用类型的链表以及数组的基本语法介绍。
1. 双向链表
回顾上一节课写的代码,当执行 addLast() 与 getLast() 方法时需要遍历链表,效率不高,因此可以添加一个指向链表末尾的索引,这样 addLast() 与 getLast() 方法的时间复杂度就为 O ( 1 ) O(1) O(1)。
但是我们再考虑一下 removeLast() 方法,如下图所示:

即使我们有了指向链表末尾的指针 last,但是当我们要移除最后一个节点时,需要的不是最后一个节点“50”的信息,而是倒数第二个节点“9”,我们需要将“9”的 next 置为 null,并将 last 指向“9”:

那么我们想要定位到这个节点“9”的唯一方法还是需要从头遍历一遍链表,同理如果你想将 last 指向链表的倒数第二个节点,认为这样就能快速定位,那么就会有新的问题:当节点“50”被删除后,如何更新 last 指向节点“3”?显然又需要从头遍历链表。
有什么办法能快速定位到这个节点呢?我们可以让每个节点不仅指向后一个节点,还能指向前一个节点,这就是双向链表(Doubly Linked List):

但是此时又会出现棘手的问题,那就是 last 指针在链表为空时会指向哨兵节点,在链表不为空时又会指向最后一个实值节点:

有什么办法能统一起来呢?能想到的第一个方案就是同样给尾部设定一个哨兵节点,就和之前的表头哨兵节点类似:

此外还有更完美的解决方案,那就是构建循环链表,这样只需要一个哨兵节点,无需指向链表末尾的指针,当链表为空时,哨兵的前一个节点和后一个节点都是指向自己,当链表不为空时哨兵的前一个结点为末尾节点,末尾节点的后一个节点为哨兵:

实现代码如下:
package CS61B.Lecture5;public class DLList {private static class IntNode {public int val;public IntNode next;public IntNode prev;public IntNode(int val, IntNode next, IntNode prev) {this.val = val;this.next = next;this.prev = prev;}}private IntNode sentinel = new IntNode(0, null, null);private int size;public DLList() {this.sentinel.next = this.sentinel.prev = sentinel;this.size = 0;}public DLList(int val) {IntNode p = new IntNode(val, this.sentinel, this.sentinel);this.sentinel.next = this.sentinel.prev = p;this.size = 1;}public int size() {return this.size;}public int getFirst() {return this.sentinel.next.val;}public void addFirst(int val) {IntNode p = new IntNode(val, this.sentinel.next, this.sentinel);this.sentinel.next.prev = p;this.sentinel.next = p;this.size++;}public void removeFirst() {if (this.size == 0) return;this.sentinel.next.next.prev = this.sentinel;this.sentinel.next = this.sentinel.next.next;this.size--;}public int getLast() {return this.sentinel.prev.val;}public void addLast(int val) {IntNode p = new IntNode(val, this.sentinel, this.sentinel.prev);this.sentinel.prev.next = p;this.sentinel.prev = p;this.size++;}public void removeLast() {if (this.size == 0) return;this.sentinel.prev.prev.next = this.sentinel;this.sentinel.prev = this.sentinel.prev.prev;this.size--;}@Overridepublic String toString() {StringBuilder res = new StringBuilder("DLList: [");IntNode p = this.sentinel;while (p.next != this.sentinel) {res.append(p.next.val);p = p.next;if (p.next != this.sentinel) res.append(", ");}res.append("]");return res.toString();}public static void main(String[] args) {DLList L = new DLList();L.addFirst(5);L.addFirst(10);System.out.println(L.getFirst()); // 10System.out.println(L); // DLList: [10, 5]L.removeFirst();System.out.println(L); // DLList: [5]System.out.println(L.size()); // 1L.addLast(20);System.out.println(L.getLast()); // 20System.out.println(L); // DLList: [5, 20]L.removeFirst();L.removeLast();System.out.println(L); // DLList: []}
}
2. 通用类型双向链表
现在我们的链表只能存放整数,如果想存放其他数据类型例如字符串,那么需要拷贝一份代码将其中的 int 修改为 String,显然这样很冗余。
如果想实现一个通用类型的数据结构,就需要引入 Java 的泛型概念,我们可以将 DLList 定义为泛型类,这样能够编写出类型安全的、可重用的代码,同时避免类型转换的繁琐操作和潜在的运行时错误。
通过在 <> 中添加类型参数用来表示泛型,类型参数通常使用单个大写字母表示,常见的命名约定如下:
T:Type(类型)E:Element(元素)K:Key(键)V:Value(值)N:Number(数字)
需要注意:
- 泛型类型参数必须是引用类型,不能是基本数据类型(如
int、double等)。如果需要使用基本数据类型,可以使用其对应的包装类(如Integer、Double)。 - 泛型类型参数不能是
final修饰的类,因为它们不能被继承。
package CS61B.Lecture5;public class DLList<T> {private class IntNode {public T val;public IntNode next;public IntNode prev;public IntNode(T val, IntNode next, IntNode prev) {this.val = val;this.next = next;this.prev = prev;}}private IntNode sentinel = new IntNode(null, null, null);private int size;public DLList() {this.sentinel.next = this.sentinel.prev = sentinel;this.size = 0;}public DLList(T val) {IntNode p = new IntNode(val, this.sentinel, this.sentinel);this.sentinel.next = this.sentinel.prev = p;this.size = 1;}public int size() {return this.size;}public T getFirst() {return this.sentinel.next.val;}public void addFirst(T val) {IntNode p = new IntNode(val, this.sentinel.next, this.sentinel);this.sentinel.next.prev = p;this.sentinel.next = p;this.size++;}public void removeFirst() {if (this.size == 0) return;this.sentinel.next.next.prev = this.sentinel;this.sentinel.next = this.sentinel.next.next;this.size--;}public T getLast() {return this.sentinel.prev.val;}public void addLast(T val) {IntNode p = new IntNode(val, this.sentinel, this.sentinel.prev);this.sentinel.prev.next = p;this.sentinel.prev = p;this.size++;}public void removeLast() {if (this.size == 0) return;this.sentinel.prev.prev.next = this.sentinel;this.sentinel.prev = this.sentinel.prev.prev;this.size--;}@Overridepublic String toString() {StringBuilder res = new StringBuilder("DLList: [");IntNode p = this.sentinel;while (p.next != this.sentinel) {res.append(p.next.val);p = p.next;if (p.next != this.sentinel) res.append(", ");}res.append("]");return res.toString();}public static void main(String[] args) {DLList<String> L = new DLList<>(); // new DLList<String>()中的String可以省略,Java会自动判断L.addFirst("World");L.addFirst("Hello");System.out.println(L.getFirst()); // HelloSystem.out.println(L); // DLList: [Hello, World]L.removeFirst();System.out.println(L); // DLList: [World]System.out.println(L.size()); // 1L.addLast("Algorithm");System.out.println(L.getLast()); // AlgorithmSystem.out.println(L); // DLList: [World, Algorithm]L.removeFirst();L.removeLast();System.out.println(L); // DLList: []}
}
注意 IntNode 类需要改为非静态的,泛型类型变量不能直接在静态方法或静态上下文中使用,因为泛型类型变量是与类的实例相关联的,而静态上下文与类的实例无关。
3. 数组
数组的大小在创建时必须指定,并且一旦创建,其大小不能改变。如果需要更大的数组,必须创建一个新的数组并复制数据。但数组通过索引直接访问元素,时间复杂度为 O ( 1 ) O(1) O(1),适合频繁读取的场景。
3.1 数组基本语法
建议每次创建数组时都使用关键字 new,因为数组也是一个 Object,在声明了数组中的变量后也可以省略 new 关键字:
package CS61B.Lecture5;import java.util.Arrays;public class ArraySyntax {public static void main(String[] args) {int[] a = new int[3];int[] b = new int[]{1, 2, 3};int[] c = {1, 2, 3};Arrays.stream(a).forEach(x -> System.out.print(x + " ")); // 0 0 0System.out.println();Arrays.stream(b).forEach(x -> System.out.print(x + " ")); // 1 2 3System.out.println();Arrays.stream(c).forEach(x -> System.out.print(x + " ")); // 1 2 3}
}
现在再来看下面这段代码:
package CS61B.Lecture5;public class ArrayBasics {public static void main(String[] args) {int[] a = null;int[] b, c;b = new int[]{1, 2, 3, 4, 5};c = b;b = new int[]{-1, 2, 5, 4, 99};c = new int[3];a = new int[0];int b_length = b.length;String[] s = new String[6];s[4] = "ketchup";s[b[3] - b[1]] = "muffins";int[] x = {9, 10, 11};System.arraycopy(x, 0, b, 3, 2);}
}
首先声明了一个名为 a 的数组引用,但是并没有调用 new 关键字,此时 Java 并没有创建空间,只是创建了用于存放数组引用的整数空间。同样 b 和 c 只是声明了一个整数数组的引用,未存放实际的数组。
之后通过初始化了一个长度为5的数组,new 关键字使得 Java 在内存中挖掘5个连续的位置用来存放这个数组的内容,并将其地址返回给变量 b。当执行 c = b 时是将数组的引用赋值给 c,因此实际上这时候 b 和 c 指向了同一个数组,如下图所示:

接下来执行的 b = new int[]{-1, 2, 5, 4, 99}; 语句使用 new 关键字重新创建了一个数组,这时候新数组返回了一个新的内存地址,此时 b 和 c 便指向了不同数组:

再看下一步的 c = new int[3]; 改变了 c 使其指向一个新的长度为3的数组:

此时最早创建的数组 {1, 2, 3, 4, 5} 消失了,因为已经没有任何引用能找到这个数组的地址了,垃圾收集器会将其清理掉永远无法再访问这个数组。
再看下一行创建了一个长度为0的数组,虽然这样几乎没什么意义,但是只是想说明一下可以这么做:

b.length 能够获取 b 所指向的数组的长度,但是从之前的图上我们没看到任何其他变量能够记录数组的长度,因此事实证明数组有一个隐秘的实例变量记录长度,通过 Java Visualizer 无法查看这个值在哪。
String 是引用数据类型,因此如果创建了数组并不能将字符串的值直接存放在数组的那个位置上,而是在其他某个位置创建一个字符串对象后再将其引用存放在数组的某个位置上。
最后一行的 System.arraycopy() 方法是将 x 数组从0开始索引取2个值(也就是 [9, 10])复制到 b 数组从3开始索引的对应位置上:

3.2 二维数组
我们创建一个4行的二维数组:
package CS61B.Lecture5;public class Array2D {public static void main(String[] args) {int[][] a = new int[4][];a[0] = new int[]{1};a[1] = new int[]{1, 1};a[2] = new int[]{1, 2, 1};a[3] = new int[]{1, 3, 3, 1};}
}
此时我们实际上是在内存中创建了5个数组,a 指向了一个长度为4的数组,这个数组中的每个位置又存放了一个指向某个一维数组的引用,如下图所示:

相关文章:
【UCB CS 61B SP24】Lecture 5 - Lists 3: DLLists and Arrays学习笔记
本文内容为构建双向循环链表、使用 Java 的泛型将其优化为通用类型的链表以及数组的基本语法介绍。 1. 双向链表 回顾上一节课写的代码,当执行 addLast() 与 getLast() 方法时需要遍历链表,效率不高,因此可以添加一个指向链表末尾的索引&am…...
软件测试与软件开发之间的关系
软件测试与软件开发的关系 软件测试(Software Testing)与软件开发(Software Development)是软件工程中的两个核心环节,它们相辅相成,确保软件的质量和功能满足需求。以下是两者之间的关系解析:…...
QT 建立一片区域某种颜色
绘制一个位于(50, 50)的200x200的红色矩形 #include "widget.h" #include "ui_widget.h" #include <QPainter>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);update(); }Widget::~Widget() {delete…...
LeetCode--23. 合并 K 个升序链表【堆和分治】
23. 合并 K 个升序链表 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。 正文 这道题有多种解决方案 堆 比较容易,又比较直观的就是堆排序,将每个节点加入最小根堆中&…...
tp6上传文件大小超过了最大值+验证文件上传大小和格式函数
问题: 最近用tp6的文件上传方法上传文件时报文件过大错误。如下所示: $file $this->request->file(file);{"code": 1,"msg": "上传文件大小超过了最大值!","data": {"code": 1,&q…...
解决 Mac 只显示文件大小,不显示目录大小
前言 在使用 mac 的时候总是只显示文件的大小,不显示文件夹的大小,为了解决问题可以开启“计算文件夹”。 步骤 1.进入访达 2.工具栏点击“显示”选项,点击 “查看显示选项” 3.勾选 显示“资源库"文件夹 和 计算所有大小 或者点击…...
分布式大语言模型服务引擎vLLM论文解读
论文地址:Efficient Memory Management for Large Language Model Serving with PagedAttention 摘要 大语言模型(LLMs)的高吞吐量服务需要一次对足够多的请求进行批处理。然而,现有系统面临困境,因为每个请求的键值…...
快速入门——Vue框架快速上手
学习自哔哩哔哩上的“刘老师教编程”,具体学习的网站为:8.Vue框架快速上手_哔哩哔哩_bilibili,以下是看课后做的笔记,仅供参考。 第一节:前端环境准备 编码工具VSCode【www.code.visualstudio.com】/WebStorm也可&am…...
机器学习,我们主要学习什么?
机器学习的发展历程 机器学习的发展历程,大致分为以下几个阶段: 1. 起源与早期探索(20世纪40年代-60年代) 1949年:Hebb提出了基于神经心理学的学习机制,开启了机器学习的先河1950年代:机器学习的…...
安卓burp抓包,bypass ssl pinning
好久好久没有发东西了。主要是懒。。。 这几天在搞apk渗透,遇到了burp无法抓包问题,觉得可以写下来。 问题描述 1. 一台安卓手机,装了面具,可以拿到root 2. 电脑上有burp,设置代理 3.手机和电脑连同一个网段&…...
【如何基于Debian构建Kali Linux】
如何基于Debian构建Kali Linux 修改apt源获取Kali的apt密钥更新并安装Kali Linux软件包添加非root用户 在Linux系统的应用领域中,Kali Linux因其在渗透测试、安全审计等方面的出色表现而备受关注。Kali Linux是一个基于Debian的Linux发行版。接下来,将介…...
Hopper架构 GEMM教程
一 使用 1.1 makefile compile:nvcc -arch=sm_90a -lcuda -lcublas -std=c++17 matmul_h100_optimal.cu -o testrun:./test加入-lcublas,不然会有函数无法被识别 二 代码分析 2.1 kernel外参数分析 2.1.1 基本参数 constexpr int BM = 64*2;constexpr int BN = 256;cons…...
CV -- 基于GPU版CUDA环境+Pycharm YOLOv8 目标检测
目录 下载 CUDA 下载 cuDNN 下载 anaconda 安装 PyTorch pycharm 搭配 yolo 环境并运行 阅读本文须知,需要电脑中有 Nvidia 显卡 下载 CUDA 打开 cmd ,输入 nvidia-smi ,查看电脑支持 CUDA 版本: 我这里是12.0,进入…...
ELK8.17部署(Ubantu24x64)
检查java环境 ELK8.x不支持java8 若无环境可执行 sudo apt install openjdk-17-jre-headless 准备安装包 官网下载地址: ELK products 搜Elasticsearch、Kibana、Logstash、Filebeat versions需一致,这里使用8.17.0 Elasticsearch Kibana Logstash Filebeat e…...
Python glob模块使用示例代码
Python 的 glob 模块位于标准库中,专门用于在文件系统中进行 文件路径模式匹配(与 Shell 中的通配符匹配类似)。它可以根据 通配符(如 *、? 和 [])来查找符合条件的文件路径。 1. glob 模块的核心功能 路径模式匹配:根据指定的通配符模式,匹配对应的文件路径。递归搜索…...
npm、pnpm和yarn有什么区别
1. 性能和速度 npm:在较早的版本中,速度较慢,尤其是在安装大型依赖集时。自npm 5以后的版本引入了缓存机制,性能有所提升。yarn:由Facebook开发,主要目标是提高安装速度。使用了缓存和并行安装(…...
Java 基础面试
final、finalize 和 finally 的不同之处? Final:是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不 能被改变。Finalize:方法是在对象被回收之前调用的方法, 给对象…...
ac的dhcp池里option43配错导致ap无法上线问题排查过程
dhcp池里ac地址配错,导致ap无法上线问题排查过程 问题:ap手动设置ac的ip正常注册在线,但dhcp获得ip和ac地址发现无法在ac上注册成功。 组网: ac旁路结构,路由器lan口地址172.16.1.1,开dhcp服务࿰…...
第1章:LangChain4j的聊天与语言模型
LangChain4J官方文档翻译与解析 目标文档路径: https://docs.langchain4j.dev/tutorials/chat-and-language-models/ 语言模型的两种API类型 LangChain4j支持两种语言模型(LLM)的API: LanguageModel:这种API非常简单,…...
Cython学习笔记1:利用Cython加速Python运行速度
Cython学习笔记1:利用Cython加速Python运行速度 CythonCython 的核心特点:利用Cython加速Python运行速度1. Cython加速Python运行速度原理2. 不使用Cython3. 使用Cython加速(1)使用pip安装 cython 和 setuptools 库(2&…...
JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...
第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
Python竞赛环境搭建全攻略
Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型(算法、数据分析、机器学习等)不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...
