【UCB CS 61B SP24】Lecture 5 - Lists 3: DLLists and Arrays学习笔记

devtools/2025/2/24 18:56:39/

本文内容为构建双向循环链表、使用 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 指针在链表为空时会指向哨兵节点,在链表不为空时又会指向最后一个实值节点:

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

实现代码如下:

java">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(数字)

需要注意:

  • 泛型类型参数必须是引用类型,不能是基本数据类型(如 intdouble 等)。如果需要使用基本数据类型,可以使用其对应的包装类(如 IntegerDouble)。
  • 泛型类型参数不能是 final 修饰的类,因为它们不能被继承。
java">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 关键字:

java">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}
}

现在再来看下面这段代码:

java">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 并没有创建空间,只是创建了用于存放数组引用的整数空间。同样 bc 只是声明了一个整数数组的引用,未存放实际的数组。

之后通过初始化了一个长度为5的数组,new 关键字使得 Java 在内存中挖掘5个连续的位置用来存放这个数组的内容,并将其地址返回给变量 b。当执行 c = b 时是将数组的引用赋值给 c,因此实际上这时候 bc 指向了同一个数组,如下图所示:

在这里插入图片描述

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

在这里插入图片描述

再看下一步的 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行的二维数组:

java">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的数组,这个数组中的每个位置又存放了一个指向某个一维数组的引用,如下图所示:

在这里插入图片描述


http://www.ppmy.cn/devtools/161423.html

相关文章

《论模型驱动架构设计方法及其应用》审题技巧 - 系统架构设计师

软件测试工程师软考论文写作框架 一、考点概述 “模型驱动架构设计及其应用”这一论题&#xff0c;主要考察了考生对模型驱动架构设计&#xff08;MDA&#xff09;这一先进软件设计方法的理解与应用能力。论题涵盖了MDA的基本概念、核心要素、实施流程及在实际项目中的应用等…

WSL2下ubuntu开启NFS服务

1. wsl2下ubuntu配置 安装 NFS 服务&#xff1a; sudo apt-get install nfs-kernel-server rpcbindnfs 配置文件/etc/exports&#xff1a; sudo vi /etc/exports打开/etc/exports 以后在后面添加如下所示内容&#xff1a; /home/mk/nfs *(rw,sync,no_subtree_check,no_root…

vue-fastapi-admin 部署心得

vue-fastapi-admin 部署心得 这两天需要搭建一个后台管理系统&#xff0c;找来找去 vue-fastapi-admin 这个开源后台管理框架刚好和我的技术栈所契合。于是就浅浅的研究了一下。 主要是记录如何基于原项目提供的Dockerfile进行调整&#xff0c;那项目文件放在容器外部&#xf…

C++ 设计模式-中介者模式

聊天室 #include <iostream> #include <string> #include <unordered_map> #include <vector>// 前向声明 class User;// 抽象中介者 class ChatMediator { public:virtual ~ChatMediator() default;virtual void broadcast(const std::string& …

【数据结构】(12) 反射、枚举、lambda 表达式

一、反射 1、反射机制定义及作用 反射是允许程序在运行时检查和操作类、方法、属性等的机制&#xff0c;能够动态地获取信息、调用方法等。换句话说&#xff0c;在编写程序时&#xff0c;不需要知道要操作的类的具体信息&#xff0c;而是在程序运行时获取和使用。 2、反射机制…

深度学习之图像回归(二)

前言 这篇文章主要是在图像回归&#xff08;一&#xff09;的基础上对该项目进行的优化。&#xff08;一&#xff09;主要是帮助迅速入门 理清一个深度学习项目的逻辑 这篇文章则主要注重在此基础上对于数据预处理和模型训练进行优化前者会通过涉及PCA主成分分析 特征选择 后…

视频HDR技术详解,你的电脑怎么播放HDR视频?

闲聊&#xff1a;前两天在b站上面看到影视飓风的视频&#xff0c;让我有点疑惑&#xff0c;我不知道为什么播放视频有设备撑不住一说&#xff0c;所以感兴趣去ytb下载了4k原片30hz刷新的&#xff0c;然后测试一下我的电脑能不能播放&#xff0c;发现还是可以的&#xff0c;视觉…

R语言安装生物信息数据库包

R语言安装生物信息数据库包 在生物信息学领域&#xff0c;R语言是重要的数据分析工具。今天&#xff0c;我们就来聊聊在R语言环境下&#xff0c;安装生物信息数据库包&#xff08;org.*.*.db&#xff09;的步骤。 为什么要安装org.*.*.db系列包 生物信息学分析中&#xff0c…