【UCB CS 61B SP24】Lecture 4 - Lists 2: SLLists学习笔记

本文内容为重写上一节课中的单链表,将其重构成更易于用户使用的链表,实现多种操作链表的方法。

1. 重构单链表SLList

在上一节课中编写的 IntList 类是裸露递归的形式,在 Java 中一般不会这么定义,因为这样用户可能需要非常了解引用,并且能够递归地思考,才能使用这个类。

因此我们需要实现一个更容易使用的单链表(Singly Linked List),首先我们定义节点类 IntNode

java 复制代码
package CS61B.Lecture4;

public class IntNode {
    public int val;
    public IntNode next;

    public IntNode(int val, IntNode next) {
        this.val = val;
        this.next = next;
    }
}

现在就可以创建单链表类 SLList

java 复制代码
package CS61B.Lecture4;

public class SLList {
    private IntNode head;

    public SLList(int val) {
        this.head = new IntNode(val, null);
    }

    public static void main(String[] args) {
        SLList L = new SLList(5);
    }
}

之前用户使用单链表需要这样定义:IntList L = new IntList(5, null);,他需要有递归考虑的思想,必须指定所指向的下一个链表的空值。

由于 IntNode 类只有在 SLList 类中使用才有意义,且用户一般不会单独去使用 IntNode 类,因此可以将其作为嵌套类放入 SLList 中,并且可以使用 private 关键字控制其权限:

java 复制代码
package CS61B.Lecture4;

public class SLList {
    private static class IntNode {
        public int val;
        public IntNode next;

        public IntNode(int val, IntNode next) {
            this.val = val;
            this.next = next;
        }
    }

    private IntNode head;

    public SLList(int val) {
        this.head = new IntNode(val, null);
    }

    public static void main(String[] args) {
        SLList L = new SLList(5);
    }
}

注意到我们还将 IntNode 类定义为静态的,静态嵌套类与外部类的实例无关,它更像是一个独立的类,但逻辑上仍然属于外部类的范畴,静态嵌套类可以直接访问外部类的静态字段和方法,但不能直接访问外部类的实例字段和方法(除非通过外部类的实例),因此静态嵌套类通常用于逻辑上与外部类相关,但不需要依赖外部类实例的场景。

2. 实现操作链表的方法

现在我们实现操作链表的几种方法:

java 复制代码
package CS61B.Lecture4;

public class SLList {
    private static class IntNode {
        public int val;
        public IntNode next;

        public IntNode(int val, IntNode next) {
            this.val = val;
            this.next = next;
        }
    }

    private IntNode head;

    public SLList(int val) {
        this.head = new IntNode(val, null);
    }

    // 获取首部节点值
    public int getFirst() {
        return this.head.val;
    }

    // 在首部添加节点
    public void addFirst(int val) {
        this.head = new IntNode(val, this.head);
    }

    // 删除首部节点并返回其值
    public int removeFirst() {
        if (this.head == null) {
            System.out.println("Already Empty!");
            return 0;
        }

        int val = this.head.val;
        this.head = this.head.next;
        return val;
    }

    // 获取尾部节点值
    public int getLast() {
        IntNode p = this.head;

        while (p.next != null) p = p.next;
        return p.val;
    }

    // 在尾部添加节点
    public void addLast(int val) {
        IntNode p = this.head;

        while (p.next != null) p = p.next;
        p.next = new IntNode(val, null);
    }

    // 删除尾部节点并返回其值
    public int removeLast() {
        if (this.head == null) {
            System.out.println("Already Empty!");
            return 0;
        }

        int val;

        if (this.head.next == null) {
            val = this.head.val;
            this.head = null;
        } else {
            IntNode p = this.head;
            while (p.next.next != null) p = p.next;
            val = p.next.val;
            p.next = null;
        }

        return val;
    }

    // 获取链表长度
    public int size() {
        return this.size(this.head);  // 无法在该方法中直接实现递归,需要一个额外的辅助方法
    }

    // size()递归辅助方法
    private int size(IntNode p) {
        if (p.next == null) return 1;
        return 1 + size(p.next);
    }

    // 重写输出SLList对象的信息
    @Override
    public String toString() {
        if (this.head == null) return "[]";

        StringBuilder res = new StringBuilder("SLList: [");
        IntNode p = this.head;

        while (p != null) {
            res.append(p.val);
            if (p.next != null) res.append(", ");
            else res.append("]");
            p = p.next;
        }

        return res.toString();
    }

    public static void main(String[] args) {
        SLList L = new SLList(5);

        L.addFirst(10);
        System.out.println(L.getFirst());  // 10

        L.addLast(15);
        System.out.println(L.getLast());  // 15

        System.out.println(L.size());  // 3
        System.out.println(L);  // SLList: [10, 5, 15]

        System.out.println(L.removeFirst());  // 10
        System.out.println(L.removeLast());  // 15
        System.out.println(L.size());  // 1
        System.out.println(L);  // SLList: [5]
    }
}

需要重点思考的是如何用递归的方式求解链表的长度,我们使用的是构造一个辅助方法 size(IntNode p) 使得用户可以通过 size() 方法直接获取链表长度。

3. 优化效率

我们现在关注 size() 方法,发现每次用户需要查看链表长度时都需要遍历一次链表,因此我们可以用一个变量实时记录链表的长度,也就是用空间换时间,这样每次查询只需要 O ( 1 ) O(1) O(1) 的时间复杂度,这也是上一节课中直接裸露递归的类 IntList 所无法实现的:

java 复制代码
package CS61B.Lecture4;

public class SLList {
    private static class IntNode {
        public int val;
        public IntNode next;

        public IntNode(int val, IntNode next) {
            this.val = val;
            this.next = next;
        }
    }

    private IntNode head;
    private int size;

    public SLList(int val) {
        this.head = new IntNode(val, null);
        this.size = 1;
    }

    // 获取首部节点值
    public int getFirst() {
        return this.head.val;
    }

    // 在首部添加节点
    public void addFirst(int val) {
        this.head = new IntNode(val, this.head);
        this.size++;
    }

    // 删除首部节点并返回其值
    public int removeFirst() {
        if (this.head == null) {
            System.out.println("Already Empty!");
            return 0;
        }

        int val = this.head.val;
        this.head = this.head.next;
        this.size--;
        return val;
    }

    // 获取尾部节点值
    public int getLast() {
        IntNode p = this.head;

        while (p.next != null) p = p.next;
        return p.val;
    }

    // 在尾部添加节点
    public void addLast(int val) {
        IntNode p = this.head;

        while (p.next != null) p = p.next;
        p.next = new IntNode(val, null);
        this.size++;
    }

    // 删除尾部节点并返回其值
    public int removeLast() {
        if (this.head == null) {
            System.out.println("Already Empty!");
            return 0;
        }

        int val;

        if (this.head.next == null) {
            val = this.head.val;
            this.head = null;
        } else {
            IntNode p = this.head;
            while (p.next.next != null) p = p.next;
            val = p.next.val;
            p.next = null;
        }

        this.size--;
        return val;
    }

    // 获取链表长度
    public int size() {
        return this.size;
    }

    // 重写输出SLList对象的信息
    @Override
    public String toString() {
        if (this.head == null) return "[]";

        StringBuilder res = new StringBuilder("SLList: [");
        IntNode p = this.head;

        while (p != null) {
            res.append(p.val);
            if (p.next != null) res.append(", ");
            else res.append("]");
            p = p.next;
        }

        return res.toString();
    }

    public static void main(String[] args) {
        SLList L = new SLList(5);

        L.addFirst(10);
        System.out.println(L.getFirst());  // 10

        L.addLast(15);
        System.out.println(L.getLast());  // 15

        System.out.println(L.size());  // 3
        System.out.println(L);  // SLList: [10, 5, 15]

        System.out.println(L.removeFirst());  // 10
        System.out.println(L.removeLast());  // 15
        System.out.println(L.size());  // 1
        System.out.println(L);  // SLList: [5]
    }
}

4. 修复addLast()

首先我们写一个无参的构造方法,这样用户能够创建一个空链表:

java 复制代码
public SLList() {
    this.head = null;
    this.size = 0;
}

现在我们尝试创建空链表,并用 addLast() 方法添加节点:

java 复制代码
SLList L = new SLList();
L.addLast(10);
System.out.println(L);

会发现程序报错了,因为空链表的 this.head 就为 null,在执行 while (p.next != null) 时无法获取 p.next,因此我们可以这样修改 addLast() 方法:

java 复制代码
// 在尾部添加节点
public void addLast(int val) {
    if (this.head == null) this.head = new IntNode(val, null);
    else {
        IntNode p = this.head;

        while (p.next != null) p = p.next;
        p.next = new IntNode(val, null);
    }
    this.size++;
}

5. 虚拟头节点

现在观察代码会发现有些方法里做了形如 if (this.head == null) 的特判,当工程代码量变大时如果习惯这么写会导致代码冗余复杂。

我们可以添加一个虚拟头节点,这样就不会存在头节点为空的情况,这种虚拟头节点也称哨兵节点,并且修改 remove 方法使其不返回节点值,因为可以通过 get 方法获取节点值。最后本节课的完整实现代码如下:

java 复制代码
package CS61B.Lecture4;

public class SLList {
    private static class IntNode {
        public int val;
        public IntNode next;

        public IntNode(int val, IntNode next) {
            this.val = val;
            this.next = next;
        }
    }

    private IntNode sentinel;  // 第一个节点在sentinel.next上
    private int size;

    public SLList() {
        this.sentinel = new IntNode(0, null);
        this.size = 0;
    }

    public SLList(int val) {
        this.sentinel = new IntNode(0, new IntNode(val, null));
        this.size = 1;
    }

    // 获取首部节点值
    public int getFirst() {
        IntNode p = this.sentinel;
        if (p.next != null) p = p.next;
        return p.val;
    }

    // 在首部添加节点
    public void addFirst(int val) {
        this.sentinel.next = new IntNode(val, this.sentinel.next);
        this.size++;
    }

    // 删除首部节点
    public void removeFirst() {
        if (this.sentinel.next == null) return;
        this.sentinel.next = this.sentinel.next.next;
        this.size--;
    }

    // 获取尾部节点值
    public int getLast() {
        IntNode p = this.sentinel;
        while (p.next != null) p = p.next;
        return p.val;
    }

    // 在尾部添加节点
    public void addLast(int val) {
        IntNode p = this.sentinel;
        while (p.next != null) p = p.next;
        p.next = new IntNode(val, null);
        this.size++;
    }

    // 删除尾部节点
    public void removeLast() {
        if (this.sentinel.next == null) return;
        IntNode p = this.sentinel;
        while (p.next.next != null) p = p.next;
        p.next = null;
        this.size--;
    }

    // 获取链表长度
    public int size() {
        return this.size;
    }

    // 重写输出SLList对象的信息
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder("SLList: [");
        IntNode p = this.sentinel;

        while (p.next != null) {
            res.append(p.next.val);
            p = p.next;
            if (p.next != null) res.append(", ");
        }

        res.append("]");
        return res.toString();
    }

    public static void main(String[] args) {
        SLList L = new SLList();
        System.out.println(L.size());  // 0
        System.out.println(L);  // SLList: []

        L.addLast(15);
        System.out.println(L.getLast());  // 15

        L.addFirst(10);
        System.out.println(L.getFirst());  // 10

        System.out.println(L.size());  // 2
        System.out.println(L);  // SLList: [10, 15]

        L.removeLast();
        L.removeFirst();
        L.removeLast();
        L.removeFirst();
        System.out.println(L.size());  // 0
        System.out.println(L);  // SLList: []
    }
}
相关推荐
Java天梯之路36 分钟前
Java 初学者必看:接口 vs 抽象类,到底有什么区别?
java·开发语言
小熊officer1 小时前
Nginx中正向代理,反向代理,负载均衡
java·nginx·负载均衡
cookqq1 小时前
mongodb根据索引IXSCAN 查询记录流程
数据结构·数据库·sql·mongodb·nosql
信码由缰1 小时前
Java 应用容器化与部署
java
摇滚侠1 小时前
2025最新 SpringCloud 教程,编写微服务 API,笔记08
笔记·spring cloud·微服务
方白羽1 小时前
Kotlin遇上Java 静态方法
android·java·kotlin
通往曙光的路上1 小时前
焚决糟糕篇
java·spring boot·tomcat
16_one1 小时前
autoDL安装Open-WebUi+Rag本地知识库问答+Function Calling
人工智能·后端·算法
狂奔小菜鸡2 小时前
Day18 | 深入理解Object类
java·后端·java ee
jiayong232 小时前
Maven NUL文件问题 - 解决方案实施报告
java·maven