【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&…...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
什么是EULA和DPA
文章目录 EULA(End User License Agreement)DPA(Data Protection Agreement)一、定义与背景二、核心内容三、法律效力与责任四、实际应用与意义 EULA(End User License Agreement) 定义: EULA即…...
C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...
uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...
