原文地址:http://www.cs.cmu.edu/~adamchik/15-121/lectures/Linked%20Lists/linked%20lists.html
引言
使用数组来存储数据的一个缺点在于,数组是静态结构,因此不能很容易地扩展或缩小以适合数据集。对数组进行插入和删除的代价也比较高。本文考虑一种名为链表的数据结构,解决了数组的一些局限性。
链表是一种线性数据结构,每个元素都是一个单独的对象。
链表的每个元素(称之为一个节点)包括两个部分—数据域和指针域(链接到下一个节点)。最后一个节点链接到null。链表的起点称为头结点。应该注意的是,头结点不是一个单独的节点,而是链接到第一个节点。如果链表为空,则头结点为空引用。
链表是一种动态数据结构。一个链表中的节点数不是固定的,可按需增多和减少。任何要处理未知数目对象的应用需要使用链表。
链表的一个缺点是它不允许直接访问各个元素。如果要访问一个特定的元素,必须从头结点开始,沿着链接,直到得到该元素。
与数组相比,链表的另一个缺点是它占用更多的内存—需要额外的4个字节(32位CPU)来存储到下一个节点的引用。
链表的类型
单链表如上所述。
双向链表有两个引用,一个链接到下一个节点,另一个链接到前面的节点。
链表的另一种重要类型称为循环链表,其中链表的最后一个节点指向第一个节点(或头结点)。
结点类
在Java中,可以在一个类(例如A)的内部定义另一个类(例如B)。A类称为外部类,B类称为内部类。内部类的目的纯粹是为了在内部作为辅助类。下面是具有内部节点类的LinkedList类:
private static class Node
{
private AnyType data;
private Node next;
public Node(AnyType data, Node next)
{
this.data = data;
this.next = next;
}
}
一个内部类是它的外部类的成员,并可访问外部类的其他成员(包括私有成员),反之亦然,即外部类可以直接访问内部类的所有成员。一个内部类可以声明为私有,公共,保护或包权限。有两种类型的内部类:静态的和非静态的。静态内部类不能直接引用实例变量或在其外部类中定义的方法,它只能通过对象引用来使用它们。
我们用两个内部类来实现LinkedList类:静态节点类和非静态LinkedListIterator类。完整的实现参看LinkedList.java。
实例
对于上面的单链表,跟踪各片段,效果如下。在每一行执行之前链表恢复到其初始状态。
1. head = head.next;
2. head.next = head.next.next;
3. head.next.next.next.next= head;
链表操作
addFirst
该方法创建一个节点,并把它加在链表的开头。
public void addFirst(AnyType item)
{
head = new Node(item, head);
}
遍历
从头结点开始,访问每个节点,直到到达null。不要更改头结点。
Node tmp = head;
while(tmp != null) tmp = tmp.next;
addLast
该方法将节点加到链表的末尾。这需要遍历,但要确保停在最后一个节点。
public void addLast(AnyType item)
{
if(head == null) addFirst(item);
else
{
Node tmp = head;
while(tmp.next != null) tmp = tmp.next;
tmp.next = new Node(item, null);
}
}
结点后插入
查找包含“key”的节点,在其后插入一个新的节点。在下图中,我们在“E”之后插入一个新的节点:
public void insertAfter(AnyType key, AnyType toInsert)
{
Node tmp = head;
while(tmp != null && !tmp.data.equals(key)) tmp = tmp.next;
if(tmp != null)
tmp.next = new Node(toInsert, tmp.next);
}
结点前插入
查找包含“key”的节点,在该节点之前插入一个新的节点。在下图中,我们在“A”之前插入一个新的节点:
为方便起见,维护两个引用(reference)prev 和cur。沿着链表移动,移动这两个引用,保持prev在cur的前一步。继续下去,直到cur到达需要在其之前插入的节点。如果cur到达null,我们不插入,否则在prev和cur之间插入一个新的节点。
观察这个实现:
public void insertBefore(AnyType key, AnyType toInsert)
{
if(head == null) return null;
if(head.data.equals(key))
{
addFirst(toInsert);
return;
}
Node prev = null;
Node cur = head;
while(cur != null && !cur.data.equals(key))
{
prev = cur;
cur = cur.next;
}
//insert between cur and prev
if(cur != null) prev.next = new Node(toInsert, cur);
}
删除
查找包含"key"的节点,并删除它。在下图中我们删除包含“A”的节点。
该算法类似于在结点前插入的算法。使用两个引用prev和cur较为方便。沿着链表移动,移动这两个引用,保持prev在cur的前一步。继续下去,直到cur到达我们需要删除的节点。我们需要考虑三种特殊情况:
1 链表为空
2 删除头节点
3 节点不在链表中
public void remove(AnyType key)
{
if(head == null) throw new RuntimeException("cannot delete");
if( head.data.equals(key) )
{
head = head.next;
return;
}
Node cur = head;
Node prev = null;
while(cur != null && !cur.data.equals(key) )
{
prev = cur;
cur = cur.next;
}
if(cur == null) throw new RuntimeException("cannot delete");
//delete cur node
prev.next = cur.next;
}
迭代器
迭代器的思想是提供访问私有聚合数据的方式,同时隐藏底层表示。Java迭代器是一个对象,因此它的实现需要创建一个实现了Iterator 接口的类。通常这样的类被实现为一个私有内部类。该Iterator接口包含下列方法:
在本节中,我们在LinkedList类中实现了Iterator。首先在LinkedList类中添加一个新的方法:
public Iterator iterator()
{
return new LinkedListIterator();
}
这里LinkedListIterator 是LinkedList类中的私有类。
private class LinkedListIterator implements Iterator
{
private Node nextNode;
public LinkedListIterator()
{
nextNode = head;
}
...
}
该LinkedListIterator类必须提供next()和hasNext()方法的实现。next()方法如下:
public AnyType next()
{
if(!hasNext()) throw new NoSuchElementException();
AnyType res = nextNode.data;
nextNode = nextNode.next;
return res;
}
复制(Cloning)
像任何其他对象一样,我们需要学习如何复制链表。如果简单地使用Object类的clone()方法,我们会得到一个名为浅拷贝的结构:
Object类的clone()将创建第一个节点的副本,并共享其他结点。这并不是我们所说的“该对象的副本”。其实我们要的是下图所代表的副本:
由于数据是不可变的,可以在两个链表之间共享数据。有几种方法来实现链表复制。最简单的一种是遍历原始链表,并使用addFirst()方法复制每个节点。这个完成后,将得到一个具有相反顺序的新链表。最后,我们一定要反转链表:
public Object copy()
{
LinkedList twin = new LinkedList();
Node tmp = head;
while(tmp != null)
{
twin.addFirst( tmp.data );
tmp = tmp.next;
}
return twin.reverse();
}
一个更好的方法是为新链表使用一个尾引用,在最后一个节点之后添加每个新节点。
public LinkedList copy3()
{
if(head==null) return null;
LinkedList twin = new LinkedList();
Node tmp = head;
twin.head = new Node(head.data, null);
Node tmpTwin = twin.head;
while(tmp.next != null)
{
tmp = tmp.next;
tmpTwin.next = new Node(tmp.data, null);
tmpTwin = tmpTwin.next;
}
return twin;
}
(多项式代数应用部分未翻译)