5. SLLists, Nested Classes, Sentinel Nodes
约 949 字大约 3 分钟
2025-04-29
上文讲到 IntList 的递归实现,代码如下:
public class IntList {
public int first;
public IntList rest;
public IntList(int f, IntList r) {
first = f;
rest = r;
}
// 初始化和添加节点:
public static void main(String[] args) {
IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
}
}
但是仍然存在许多问题,我们来逐一解决。
改进一:重新命名
把 IntList 改名为 IntNode,至于为什么下一部分会讲。
public class IntNode {
public int item;
public IntNode next;
public IntNode(int i, IntNode n) {
item = i;
next = n;
}
}
改进二 :一切操作交给 SLList
// 初始化和添加节点:
public static void main(String[] args) {
IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
}
之前的初始化和添加节点的操作很不好:IntList L = new IntList (15, null);
有个 null 指针;L = new IntList (10, L);
每次都要添加 L 在第二个参数,很不方便也不优雅。
所以我们可以采用新建一个 SLList 类,来负责管理 IntNode。就像是故宫博物馆门口排队,得需要一个工作人员引导游客来排队,不然乱糟糟的。
public class SLList {
public IntNode first;
public SLList(int x) {
first = new IntNode(x, null);
}
}
这个时候的初始化链表操作就好看多啦:
SLList L = new SLList(5);
接下来写一下添加节点的指令:
public void addFirst(int x) {
first = new IntNode(x, first);
}
这个时候的添加节点操作也好看多啦:
SLList L = new SLList(5);
L.addFirst(10);
L.addFirst(15);
改进三 :修改访问控制符
有个很尴尬的事情:你似乎可以让最后一个节点的 item 指向自己,那么这样 size 函数就会无限循环下去...
SLList L = new SLList(15);
L.addFirst(10);
L.first.next.next = L.first.next;
这可不行:这不仅看着不优雅,而且也会带来一系列安全问题。解决方法就是:在 SLList 外部的对象,不能访问 SLList 的成员变量。因此直接把成员变量直接设置成 private。
在 Java 开发中,为了保证这种安全(或者说为了实现 API,会隐藏一个类的内部情况,只把接口露出给外界访问),成员变量一般设置为 private,然后写个这个变量的 getter、setter 方法,并且把这两种方法设置成 public,给外界使用。
改进四:嵌套类
public class SLList {
/** SLList 内部嵌套了一个 IntNode 类
/* 这里使用了 static,会导致 IntNode 里面的所有方法
/* 无法访问SLList的内容,这样可以节省一点内存 */
public static class IntNode {
public int item;
public IntNode next;
public IntNode(int i, IntNode n) {
item = i;
next = n;
}
}
private IntNode first;
public SLList(int x) {
first = new IntNode(x, null);
}
...
接下来加上 addLast 函数:
/** Adds an item to the end of the list. */
public void addLast(int x) {
IntNode p = first;
/* Advance p to the end of the list. */
while (p.next != null) {
p = p.next;
}
p.next = new IntNode(x, null);
}
改进五 :cache
还记得 size 函数吗?它还是递归运行的,时间复杂度为 O(N),其实还可以优化。
我们可以给 SLList 设置一个 size 变量,在 add 的时候进行 size++
,运行 size 函数的时候直接返回 size 变量就行,时间复杂度 O(N)。
public class SLList {
/* IntNode declaration omitted. */
private IntNode first;
private int size;
public SLList(int x) {
first = new IntNode(x, null);
size = 1;
}
public void addFirst(int x) {
first = new IntNode(x, first);
size += 1;
}
public int size() {
return size;
}
...
}
改进六 :哨兵
有个问题是:假如我们的链表没有存放任何元素,也就是 SLList 为空,那么 addLast 执行的时候会指向这个空值导致报错,这怎么解决?
解决方法就是在开头加上一个“无用”的节点,我们叫它哨兵。这样 SLList 的 first 成员就不为空了,可以继续递归下去。