链表是一种新的数据存储结构,有别于低效的、大小不可变的数组,它被广泛地应用在许多通用的数据库中。除非需要频繁的通过下标随机访问各个数据,否则在很多使用数组的地方都可以用链表代替。
一、链结点Link
每个数据项都被包含在“链结点”中。一个链结点是某个类的对象,这个类可以叫做Link。每个Link对象中都包含一个对下一个链结点引用的字段(next)。链表本身的对象中有一个字段指向对第一个链结点的引用。
class Link {
public int iData;
public Link next;
}
这是一种自引用式的类,因为它包含了一个和自己类型相同的字段。
Q:这里我们提一个问题。在Link的类定义中定义一个Link类型的域,编译器怎样才能不混淆呢?编译器在不知道一个Link对象占多大空间的情况下,如何知道一个包含相同对象的Link对象占多大空间?
A:在Java中,Link对象并没有真正包含另外一个Link对象。类型为Link的next字段仅仅是对另外一个Link对象的“引用”,而不是一个对象。一个引用是一个对某个对象的参照数值,它是一个计算机内存中的对象地址,然而不需要知道它的具体值。在给定的操作系统中,所有的引用,不管它指向谁,大小都是一样的。因此,对编译器来说,知道这个字段的大小并由此构造出整个Link对象,是没有任何问题的。
所以,这里可以看出链表和数组的差别。在一个数组中,每一项占用一个特定的位置。这个位置可以用一个下标号直接访问。就像一排房子,你可以凭地址找到其中特定一间。但是在链表中,寻找一个特定元素的唯一方法就是沿着这个元素的链一直向下寻找,每个元素之间都有关系。
二、单链表的Java代码
class Link { //链结点
public int iData;
public Link next;
public Link(int value) {
iData = value;
}
public void displayLink() {
System.out.print(iData + “ “);
}
}
class LinkList { //链表
private Link first;//链表的第一个链结点,它是惟一的需要维护的永久信息,用以定位其它链结点
public LinkList() {
}
public boolean isEmpty() {
return first == null;
}
public void insertFirst(int id) {
Link newLink = new Link(id);
newLink.next = first; //newLink - -> old first
first = newLink; //first - -> newLink
}
public Link deleteFirst() {
Link temp = first;
first = first.next; //删除:first - -> old next
return temp;
}
//查找到包含指定关键字的链结点
public Link find(int key) {
Link current = first;
while(current.iData != key) {
if(current.next == null)
return null;
else
current = current.next;
}
return current;
}
//删除包含指定关键字的链结点
public Link delete(int key) {
Link current = first;
Link previous = first;
while(current.iData != key) {
if(current.next == null)
return null;
else {
previous = current;
current = current.next;
}
if(current == first)
first = first.next;
else
previous.next = current.next; //被删除的节点的链在此处断开
return current;
}
}
public void displayList() {
System.out.print(“List (first - -> last): “);
Link current = first;
while(current != null) {
current.displayLink();
current = current.next;
}
}
}
三、双端链表
双端链表新增一个特性:即对最后一个链结点的引用,就像对第一个链结点的引用一样。像表头一样访问表尾的特性,使双端链表更适合于一些普通链表不方便操作的场合,如队列。
class Link {
public long data;
public Link next;
public Link(long value) {
data = value;
}
public void displayLink() {
System.out.print(data + “ “);
}
}
class FirstLastLinkList {
public Link first; //指向链表中第一个链结点
public Link last; //指向链表中最后一个链结点
//如果链表中只有一个链结点,两者都指向它,如果没有链结点,两者都是null
public boolean isEmpty {
return first == null;
}
public void insertFirst(long data) {
Link newLink = new Link(data);
if (isEmpty()) //如果链表是空的,必须把last指向新的链结点
last = newLink; //last引用到表尾
newLink.next = first;
first = newLink; //从表头插入,指向新的链结点
}
public void insertLast(long data) {
Link newLink = new Link(data);
if (isEmpty()) //如果链表是空的,必须把first指向新的链结点
first = newLink; //first引用到表头
else
last.next = newLink;
last = newLink; //从表尾插入,指向新的链结点
}
public long deleteFirst() {
long temp = first.data;
if(first.next == null) //如果只有一个数据项
last = null; //last - ->null
first = first.next; //first - ->old next
return temp;
}
public void displayList() {
System.out.print(“List (first - ->last): “);
Link current = first;
while(current != null) {
current.displayLink();
current = current.next;
}
}
}
双端链表中,在表头重复插入操作会颠倒链结点进入的顺序,比如插入1,2,3,在表中是3,2,1.但是在表尾重复插入操作则保持链结点进入的顺序。
然而,双端链表并不能高效地删除最后一个链结点,因为没有一个引用指向倒数第二个链结点。如果最后一个链结点被删除,倒数第二个链结点的next字段应该变成null。
四、链表的效率
在表头插入和删除速度很快,仅需要改变一两个引用值,所以花费O(1)的时间。平均起来,查找、删除和插入都需要搜索链表中的一半链结点,需要O(N)次比较,在数组中执行这些操作也是O(N)次比较。当然,链表更快,因为链表不需要移动任何东西。此外,链表需要用多少内存就可以用多少内存,并且可以扩展到所有可用内存。而数组的大小在它创建时就固定了。
向量是一种可扩展的数组,它可以通过可变长度解决这个问题,但是它经常只允许以固定大小的增量扩展,比如快要溢出时增加一倍数组容量。
五、抽象数据类型(ADT)
它是一种考虑数据结构的方式:着重于它做了什么,而忽略它是怎么做的。栈和队列都是ADT。
class Link {
public long data;
public Link next;
public Link(long value) {
data = value;
}
}
1、用链表实现的栈
class LinkList {
private Link first;
public boolean isEmpty() {
return first == null;
}
public void insertFirst(long value) {
Link newLink = new Link(value);
newLink.next = first;
first = newLink;
}
public long deleteFirst() {
Link temp = first;
first = first.next;
return temp.data;
}
}
class LinkStack {
private LinkList list;
public LinkStack() {
list = new LinkList();
}
public void push(long value) {
list.insertFirst(value);
}
public long pop() {
return list.deleteFirst();
}
public boolean isEmpty() {
return list.isEmpty();
}
}
2、用链表实现的队列
class FirstLastList {
private Link first;
private Link last;
public boolean isEmpty() {
return first == null;
}
//从表尾插入数据,可以实现进入队列的效果
public void insertLast(long value) {
Link newLink = new Link(value);
if(isEmpty())
first = newLink;
else
last.next = newLink;
last = newLink;
}
//从表头弹出数据,可以实现弹出队列的效果
public long deleteFirst() {
long temp = first.data;
if(first.next == null)
last = null;
first = first.next;
return temp;
}
}
class LinkQueue {
private FirstLastList list;
public LinkQueue() {
list = new FirstLastList();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void insert(long value) {
list.insertLast(value);
}
public long remove() {
return list.deleteFirst();
}
}
六、有序链表
数据按照关键值有序排序的。可以用来实现优先级队列。在有序链表插入一个数据,关键在于遍历链表,寻找合适的插入位置,把新链结点的next字段指向下一个链结点,把前一个链结点的next字段指向新的链结点。
class SortedList {
private Link first;
public boolean isEmpty() {
return first == null;
}
public void insert(long data) {
Link newLink = new Link(data);
Link previous = null;
Link current = first;
while(current != null && key > current.data) {
previous = current;
current = current.next;
}
if(previous == null)
first = newLink;
else
previous.next = newLink;
newLink.next = current;
}
public Link remove() {
Link temp = first;
first = first.next;
return temp;
}
}
在有序链表中插入或删除数据必须沿着链表一步一步找到正确位置,因此需要O(N)次比较,平均N/2。然而只需要O(1)的时间就可以找到或删除最小值。
七、双向链表
为什么使用双向链表?因此传统的链表难以沿链表的反向遍历。
class Link {
public long data;
public Link next;
public Link previous;
public Link(long value) {
data = value;
}
}
双向链表的缺点是每次插入或删除一个链结点的时候,要处理四个链结点的引用,因此链结点占用空间也变大了。
class DoubleLinkedList {
private Link first;
private Link last;
public boolean isEmpty() {
return first == null;
}
public void insertFirst(long value) {
Link newLink = new Link(value);
if(isEmpty())
last = newLink;
else
first.previous = newLink;
newLink.next = first;
first = newLink;
}
public void insertLast(long value) {
Link newLink = new Link(value);
if(isEmpty())
first = newLink;
else {
last.next = newLink;
newLink.previous = last;
}
last = newLink;
}
public Link deleteFirst() {
Link temp = first;
if(first.next == null)
last = null;
else
first.next.previous = null;
first = first.next;
return temp;
}
public Link deleteLast() {
Link temp = last;
if(first.next == null)
first = null;
else
last.previous.next = null;
last = last.previous;
return temp;
}
public boolean insertAfter(long key, long value) {
Link current = first;
while(current.data != key) {
current = current.next;
if(current == null)
return false;
}
Link newLink = new Link(value);
if(current == last) {
newLink.next = null;
last = newLink;
} else {
newLink.next = current.next;
current.next.previous = newLink;
}
newLink.previous = current;
current.next = newLink;
return true;
}
public Link deleteKey(long key) {
Link current = first;
while(current.data != key) {
current = current.next;
if(current == null)
return null;
}
if (current == first)
first = current.next;
else
current.previous.next = current.next;
if(current == last)
last = current.previous
else
current.next.previous = current.previous;
return current;
}
public void displayForward() {
Link current = first;
while(current != null) {
current.displayLink();
current = current.next;
}
}
public void displayBackward() {
Link current = last;
while(current != null) {
current.displayLink();
current = current.previous;
}
}
}
八、迭代器
假定你要遍历一个链表,并在某些特定的链结点上执行一些操作。在数组中,这些操作很容易实现,因为可以利用数组下标。可是链表并没有下标,你要从表头开始考察每个链结点。
作为类的用户,需要能存取指向任意链结点的引用。这样就可以考察和修改链结点。引用应该能递增,因此可以沿着整个链表遍历,依次查看每个链结点,而且可以访问这个引用所指向的链结点。这里,我们利用迭代器来包含对数据结构中数据项的引用。
class LinkList {
private Link first;
public Link getFirst() {
return first;
}
public void setFirst(Link link) {
first = link;
}
public ListIterator getIterator() {
return new ListIterator(this);
}
}
class ListIterator {
private Link current;
private Link previous;
private LinkList ourList;
public ListIterator(LinkList list) {
ourList = list;
reset();
}
public void reset() {
current = ourList.getFirst;
previous = null;
}
public boolean atEnd() {
return current.next == null;
}
public void nextLink() {
previous = current;
current = current.next;
}
public Link getCurrent() {
return current;
}
public void insertAfter(long data) {
Link newLink = new Link(data);
if(ourList.isEmpty()) {
ourList.setFirst(newLink);
current = newLink;
} else {
newLink.next = current.next;
current.next = newLink;
nextLink();
}
}
public void insertBefore(long data) {
Link newLink = new Link(data);
if(previous == null) {
newLink.next = ourList.getFirst();
ourList.setFirst(newLink);
reset();
} else {
newLink.next = previous.next;
previous.next = newLink;
current = newLink;
}
}
public long deleteCurrent() {
long value = current.data;
if(previous == null) {
ourList.setFirst(current.next);
reset();
} else {
previous.next = current.next;
if(atEnd())
reset();
else
current = current.next;
}
return value;
}
}