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

news2025/7/16 0:31:54

本文内容为构建双向循环链表、使用 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--;
    }

    @Override
    public 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());  // 10
        System.out.println(L);  // DLList: [10, 5]

        L.removeFirst();
        System.out.println(L);  // DLList: [5]
        System.out.println(L.size());  // 1

        L.addLast(20);
        System.out.println(L.getLast());  // 20
        System.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 修饰的类,因为它们不能被继承。
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--;
    }

    @Override
    public 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());  // Hello
        System.out.println(L);  // DLList: [Hello, World]

        L.removeFirst();
        System.out.println(L);  // DLList: [World]
        System.out.println(L.size());  // 1

        L.addLast("Algorithm");
        System.out.println(L.getLast());  // Algorithm
        System.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 0
        System.out.println();
        Arrays.stream(b).forEach(x -> System.out.print(x + " "));  // 1 2 3
        System.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 并没有创建空间,只是创建了用于存放数组引用的整数空间。同样 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行的二维数组:

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.coloradmin.cn/o/2303578.html

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

相关文章

Git 工作流程

1、Git 工作流程 http://www.ruanyifeng.com/blog/2015/12/git-workflow.html git push -f origin HEAD^:master 删除服务器上最近的一次提交git push -f origin HEAD^:master 2、Git分支管理 动画形式演示分支效果&#xff1a; http://onlywei.github.io/explain-git-with-…

DeepSeek接入Siri(已升级支持苹果手表)完整版硅基流动DeepSeek-R1部署

DeepSeek接入Siri&#xff08;已升级支持苹果手表&#xff09;完整版硅基流动DeepSeek-R1部署 **DeepSeek** 是一款专注于深度学习和人工智能的工具或平台&#xff0c;通常与人工智能、机器学习、自动化分析等领域有关。它的主要功能可能包括&#xff1a;深度学习模型搜索&…

个人博客5年回顾

https://huangtao01.github.io/ 五年前&#xff0c;看程序羊的b站视频做的blog&#xff0c;受限于网络&#xff0c;只能单向学习&#xff0c;没有人指导与监督&#xff0c;从来没有想过&#xff0c;有没有什么问题&#xff1f; 一、为什么要做个人博客&#xff1f; 二、我是怎么…

nacos编写瀚高数据库插件

1、下载nacos源码 git clone gitgithub.com:alibaba/nacos.git 2、引入瀚高驱动 <dependency><groupId>com.highgo</groupId><artifactId>jdbc</artifactId><version>${highgo.version}</version></dependency> 3、DataSource…

bboss v7.3.5来袭!新增异地灾备机制和Kerberos认证机制,助力企业数据安全

ETL & 流批一体化框架 bboss v7.3.5 发布&#xff0c;多源输出插件增加为特定输出插件设置记录过滤功能&#xff1b;Elasticsearch 客户端新增异地双中心灾备机制&#xff0c;提升框架高可用性&#xff1b;Elasticsearch client 和 http 微服务框架增加对 Kerberos 认证支持…

《Python实战进阶》专栏 No2: Flask 中间件与请求钩子的应用

专栏简介 《Python实战进阶》专栏共68集&#xff0c;分为 模块1&#xff1a;Web开发与API设计&#xff08;共10集&#xff09;&#xff1b;模块2&#xff1a;数据处理与分析&#xff08;共10集&#xff09;&#xff1b;模块3&#xff1a;自动化与脚本开发&#xff08;共8集&am…

Redis三剑客解决方案

文章目录 缓存穿透缓存穿透的概念两种解决方案: 缓存雪崩缓存击穿 缓存穿透 缓存穿透的概念 每一次查询的 key 都不在 redis 中&#xff0c;数据库中也没有。 一般都是属于非法的请求&#xff0c;比如 id<0&#xff0c;比如可以在 API 入口做一些参数校验。 大量访问不存…

Frp部署文档

Frp部署文档 开源项目地址:https://github.com/fatedier/frp项目中文文档地址&#xff1a;https://github.com/fatedier/frp/blob/dev/README_zh.md官网文档地址: https://gofrp.org/zh-cn/docs/发布包地址&#xff1a;https://github.com/fatedier/frp/releases 要注意对应的…

创建一个简单的spring boot+vue前后端分离项目

一、环境准备 此次实验需要的环境&#xff1a; jdk、maven、nvm和node.js 开发工具&#xff1a;idea或者Spring Tool Suite 4&#xff0c;前端可使用HBuilder X&#xff0c;数据库Mysql 下面提供maven安装与配置步骤和nvm安装与配置步骤&#xff1a; 1、maven安装与配置 1…

Spring Boot项目@Cacheable注解的使用

Cacheable 是 Spring 框架中用于缓存的注解之一&#xff0c;它可以帮助你轻松地将方法的结果缓存起来&#xff0c;从而提高应用的性能。下面详细介绍如何使用 Cacheable 注解以及相关的配置和注意事项。 1. 基本用法 1.1 添加依赖 首先&#xff0c;确保你的项目中包含了 Spr…

124.二叉树中的最大路径和 python

二叉树中的最大路径和 题目题目描述示例 1&#xff1a;示例 2&#xff1a;提示&#xff1a; 题解解决方案步骤Python 实现解释提交结果 题目 题目描述 二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多…

23.1 WebBrowser控件

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 WebBrowser控件类似于IE浏览器的文档界面&#xff08;事实上IE也是使用的这个控件&#xff09;&#xff0c;它提供了显示网页及支持…

vue 手写分页

【先看效果】 &#xff08;1&#xff09;内容小于2页 不展示页码 &#xff08;2&#xff09;1 < 内容页数< 限定展示页码 展示&#xff1a;页码、上下页&#xff1b;隐藏&#xff1a;首页、末页图标&#xff0c;上、下一区间码。即&#xff1a;&#xff08;页数&#…

位运算,双指针,二分,排序算法

文章目录 位运算二进制中1的个数题解代码我们需要0题解代码 排序模版排序1题解代码模版排序2题解代码模版排序3题解代码 双指针最长连续不重复子序列题解代码 二分查找题解代码 位运算 1. bitset< 16 >将十进制数转为16位的二进制数 int x 25; cout << bitset<…

Typora软件(Markdown编辑器)详细安装教程(附补丁包)2025最详细图文教程安装手册

目录 前言&#xff1a;Typora是干什么的&#xff1f; 一、下载Typora安装包 二、安装Typora 1.运行安装程序 2.启动安装 3.创建桌面图标 4.开始安装 5.安装完成 三、安装补丁 1.解压补丁包 2.在解压后的补丁包目录下找到“winmm.dll” 3.复制“winmm.dll”到Typora安…

图谱洞见:专栏概要与内容目录

文章目录 图谱洞见&#x1f4da; 核心内容模块时空图模型研究综述与模型对比交通流量预测 知识图谱理论研究预训练语言模型与知识图谱知识图谱补全与链接预测知识蒸馏与知识表示关系建模与图卷积上下文感知与参数生成规则学习与推理可解释性研究因果推理 知识图谱实践应用数据库…

【拜读】Tensor Product Attention Is All You Need姚期智团队开源TPA兼容RoPE位置编码

姚期智团队开源新型注意力&#xff1a;张量积注意力&#xff08;Tensor Product Attention&#xff0c;TPA&#xff09;。有点像一种「动态的LoRA」&#xff0c;核心思路在于利用张量分解来压缩注意力机制中的 Q、K、V 表示&#xff0c;同时保留上下文信息&#xff0c;减少内存…

【电机控制器】ESP32-C3语言模型——DeepSeek

【电机控制器】ESP32-C3语言模型——DeepSeek 文章目录 [TOC](文章目录) 前言一、简介二、代码三、实验结果四、参考资料总结 前言 使用工具&#xff1a; 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、简介 二、代码 #include <Arduino.h&g…

Linux修改主机名称

hostnamectl set-hostname 主机名称 exit 退出登录重新进入即可

STM32 看门狗

目录 背景 独立看门狗&#xff08;IWDG&#xff09; 寄存器访问保护 窗口看门狗&#xff08;WWDG&#xff09; 程序 独立看门狗 设置独立看门狗程序 第一步、使能对独立看门狗寄存器的写操作 第二步、设置预分频和重装载值 第三步、喂狗 第四步、使能独立看门狗 喂狗…