本篇主要介绍一种新的数据存储结构——链表。链表可能是继数组之后第二种使用得最广泛的通用存储结构。
链表的机制灵活,用途广泛,适用于许多通用数据库。它也可以取代数组,作为其他存储结构的基础,例如栈,队列。除非需要频繁通过下标随机访问各个数据,否则在很多使用数组的地方都可以用链表代替。
链结点
在链表中,每个数据项都被包含在“链结点”(Link)中。一个链结点是某个类的对象,这个类可以叫做Link。因为一个链表中有许多类似的链结点,所以有必要用一个不同于链表的类来表达链结点。每个Link对象中都包含一个对下一个链结点引用的字段(通常叫做next)。但是链表本身的对象中有一个字段指向对第一个链结点的引用。如下图所示,显示了这个关系:
下面是一个Link类定义的一部分。它包含了一些数据和下一个链结点的引用:
class Link {
public int data;
public int idata;
public Link next;//对下一个链结点的引用
}
这种类定义有时候叫做“自引用”式,因为它包含了一个和自己类型相同的字段(本例中叫做next)。
链结点中仅仅包含两个数据项,data和idata。但是在真正应用中,可能包含更多数据项。通常,如果数据项很多的话,可以用一个包含这些数据项的类来替代这些数据项。例如,一个人的姓名,电话,身份证号,性别等,可以使用一个Person对象来替代:
class Link {
public Person person;
public Link next;
}
单链表
//Link类的数据结构
class Link {
public int data;
public int idata;
public Link next;
//带参构造函数
public Link(int id, int da) {
idata=id;
data=da;
}
//显示节点的信息
public void displayLink(){
System.out.println("{"+idata+","+data+"}");
}
}
//LinkList数据结构
class ListLink{
private Link first; //对链表中第一个链结点的引用
public ListLink() {
first=null;
}
//是否为空链表
public boolean isEmpty(){
return (first==null);
}
public void insertFirst(int id,int data){
//先给新的链分配空间
Link newLink = new Link(id, data);
newLink.next = first;
first = newLink;
}
public Link deleteFirst(){
Link temp = first;
first = first.next;
return temp;
}
//第一种查找方法
public Link find_me(int id){
Link current = first;
while(current!=null ){
if(current.idata==id)
return current;
else
current = current.next;
}
return null;
}
//第二种查找方法
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 previous = first;
Link current = first;
//找到需要删除的节点
while(current.idata!=key){
if(current.next==null)
return null;
else{
previous = current; //对前一个Link的引用
current = current.next;
}
}
if(current==first) //首节点
first = first.next;
else //非首节点
previous.next=current.next;
return current;
}
//打印出LinkList
public void displayList(){
System.out.println("first-->last");
Link current =first;
while(current!=null){
current.displayLink();
current=current.next;
}
System.out.println(" ");
}
}
//main
public class LinkList {
public static void main(String[] args) {
ListLink theList= new ListLink();
//插入链结点 头插法
theList.insertFirst(22, 299);
theList.insertFirst(44, 499);
theList.insertFirst(66, 699);
theList.insertFirst(88, 899);
theList.displayList();
//查找id为33的链结点
Link find_id = theList.find(33);
//如果找到
if(find_id!=null)
System.out.println("find_id = "+find_id.idata);
//未找到
else
System.out.println("can't find the id");
//删除id为44的链结点
Link del_link = theList.delete(44);
//存在并且删除
if(del_link!=null){
System.out.println("successfully deleted!");
theList.displayList();
}
//不存在
else
System.out.println("can't delete!");
}
}
以下是对各类和方法的解释:
Link类:
class Link {
public int data;
public int idata;
public Link next;
//构造函数
public Link(int id, int da) {
idata=id;
data=da;
}
public void displayLink(){
System.out.println("{"+idata+","+data+"}");
}
}
在构造函数初始化时,并不需要初始化next字段,因为默认是为null值。然而,为了清晰起见,也可以明确把他赋值为null。null值意味着这个字段不指向任何结点,除非该链结点后来被连接到其他的链结点才改变。
ListLink类:
class ListLink{
private Link first; //对链表中第一个链结点的引用
public ListLink() {
first=null;
}
//是否为空链表
public boolean isEmpty(){
return (first==null);
}
.....methods
}
ListLink的构造函数将first赋值成null值,这其实并不是必需的,因为在缺省状态下,会有默认一个构造函数自动进行赋值。isEmpty()方法用来判断是否为空链表。
insertFirst()方法:
public void insertFirst(int id,int data){
//先给新的链分配空间
Link newLink = new Link(id, data);
newLink.next = first;//指向first指向的节点
first = newLink;//改变first指向的节点,将first指向新的链结点
}
此方法的作用是在表头插入一个新链结点。因为first已经指向第一个链结点,为了插入新链结点,只需要使新创建的链结点的next字段等于原来的first的值,然后first指向新的链结点即可。
deleteFirst()方法:
public Link deleteFirst(){
Link temp = first;//需要删除的节点
first = first.next;//将first指向需要删除的节点的下一个节点
return temp;//返回删除的节点
}
需要注意的是,假设链表不是空的,才能进行删除操作,所以在删除操作之前,需要先判断链表是否为空,程序首先应该调用isEmpty()方法核实这一点。
在C++和类似的语言中,在从链表中取下一个链结点后,需要考虑如何删除这个链结点。它仍然存在内存中的某个地方,但是现在没有任何东西指向它。如何处理它?在java语言中,垃圾收集进程将在未来的某个时刻销毁它。
find(int key)方法:
//这种写法稍微繁了点
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 find(int key){
Link current = first;
while(current.idata!=key){
current=current.next;
if(current==null)
return null;
}
return current;
}
首先,定位到首节点,也就是first,当该节点的id不是需要查找的key时,如果该节点的next为null则代表已经到了链表的尾部,还是没有找到,则返回null,否则,指针后移一位。
delete(int key)方法:
public Link delete(int key){
Link previous = first;
Link current = first;
//找到需要删除的节点
while(current.idata!=key){
if(current.next==null)
return null;
else{
previous = current; //对前一个Link的引用
current = current.next;
}
}
if(current==first) //首节点
first = first.next;
else //非首节点
previous.next=current.next;
return current;
}
delete方法需要先找到特定的链结点,不同的是,需要两个指针,一个指向当前链结点(current),另一个指向当前链结点的前一个链结点(previous),因为如果需要删除当前链结点(current),那么当前链结点的前一个链结点(previous)应该指向当前链结点的下一个链结点(current.next)。所以,在while语句的每一次轮循环中,每当current变量赋值为current.next之前,先把previous变量赋值为current,保证了previous总是指向current所指链结点的前一个链结点。
如果删除的是第一个链结点,这是一种特殊情况,因为这是由ListLink对象的first域指向的链结点,而不是别的链结点的next字段指向的。在这种情况下,使first字段指向first.next,就可以删除第一个链结点。
if(current==first) //首节点
first = first.next;
else //非首节点
previous.next=current.next;
链表的效率:
在表头插入和删除速度很快。仅需要改变一两个引用值,所以花费为O(1)的时间。
平均起来,查找、删除和在指定链结点后面插入都需要搜索链表中的一半链结点。需要O(N)次比较。在数组中执行这些操作也需要O(N)次比较,但链表仍然要快一些,因为当插入和删除链结点时,链表不需要移动任何东西。
当然,链表比数组优越的另外一个重要方面是链表需要多少内存就可以用多少内存,并且可以扩展到所有可用内存。而数组的大小在创建时就已经确定了,所以经常由于数组太大导致效率低下,或者数组太小导致空间溢出。
向量是一种可扩展的数组,它可以通过可变长度解决这个问题,但是它经常只允许以固定大小的增量扩展(例如快要溢出的时候,就增加一倍数组容量)。这个解决方案在内存使用效率上来说还是比链表的低。
有序链表
在有序链表中,数据是按照关键值有序排列的。有序链表的删除常常是只限于删除在链表头部的最小(或者最大)链结点。不过,有时也会用find()和delete()方法在整个链表中搜索某一特定点。
有序链表优于有序数组的地方是插入的速度(元素不需要移动),另外链表可以扩展到全部有效的使用内存,无需像数组一样事先需要分配一个固定大小。
有序链表也可以用于实现优先级队列,尽管堆是更常用的实现方法。
class Link{
public int id;
public Link next;
//构造函数
public Link(int idata) {
id = idata;
}
public void displayLink(){
System.out.println("{"+id+"}");
}
}
class SortedLinkList{
private Link first;
public SortedLinkList() {
first = null;
}
public boolean isEmpty(){
return (first==null);
}
public void insertLink(int id){
//申请一个新节点
Link newLink = new Link(id);
Link current = first;
Link previous = null;
//插入的节点大于当前节点的id
while(current!=null && id>current.id){
//previous保存current的上一个链结点
previous=current;
//往后移指针
current = current.next;
}
//表示空链表 此时只需改变first的值即可
if(previous==null)
first=newLink;
//否则 在previous与current之间 插入新链结点
else
previous.next=newLink;
//在nerLink后面插入current链结点
newLink.next=current;
}
//删除第一个节点
public Link remove(){
Link temp=first;
first = first.next;
return temp;
}
public void displayList(){
System.out.println("{dislpayList}: ");
Link current = first;
while(current!=null){
System.out.print(","+current.id);
current = current.next;
}
System.out.println("");
}
}
public class SortedListApp {
public static void main(String[] args) {
SortedLinkList sorted = new SortedLinkList();
sorted.insertLink(2);
sorted.insertLink(71);
sorted.insertLink(34);
sorted.insertLink(9);
sorted.insertLink(1);
sorted.displayList();
//删除第一个链结点
sorted.remove();
sorted.displayList();
}
}
有序链表的效率
在有序链表插入和删除某一项最多需要O(N)次比较(平均N/2),因为必须沿着链表上一步一步走才能找到正确的位置。然而,可以在O(1)的时间内找到或删除最小值或者最大值(从大到小排序,表头是最大,从小到大排序,表头是最小),因为它总在表头。如果一个应用频繁地存取最小项或者最大项,且不需要快速的插入,那么有序链表是一个有效的方案选择。
双向链表
先来看张图,解释一切:
PS:双向链表并不一定是双端链表,即首链结点并不一定有last指针指向尾部
双向链表,顾名思义,即提供一个指针指向前方,一个指针指向后方。在以往的单链表中,当我们访问当前链结点(current)时,我们又需要访问上一个链结点(previous),这个时候有两种办法,第一个方法是
定义两个指针,一个指针指向
current,另一个指针指向
previous(当current=current.next之前,将current赋值给previous即可),另外一个方法是直接使用
current.previous即可得到(
Link中需定义Link previous)。
双向链表,即提供
允许向前遍历,也允许向后遍历整个链表。其中秘密在于每个链结点有两个指向其他链结点的引用,而不是一个。第一个像普通链表一样指向下一个链结点。第二个指向前一个链结点,如上图所示。
class DLink{
public int id;
public DLink next;
public DLink previous;
public DLink(int key) {
id=key;
}
public void displayDLink(){
System.out.print(id+",");
}
}
class DoubleLinkList{
private DLink first;
private DLink last;
public DoubleLinkList() {
first=null;
last=null;
}
public boolean isEmpty(){
return (first==null);
}
//在头部插入一个链结点
public void insertFirst(int key) {
DLink newLink = new DLink(key);
if (isEmpty())
last = newLink;
else
first.previous = newLink;
// previous默认是null 所以无需注明newLink.previous=null
newLink.next = first;
first = newLink;
}
public void insertLast(int key){
DLink newLink = new DLink(key);
if(isEmpty())
first=newLink;
else{
last.next=newLink;
newLink.previous=last;
}
last=newLink;
}
//在特定的节点后面插入, key为特定节点,insertID为插入的节点
public boolean insertAfter(int key,int insertID){
DLink newLink = new DLink(insertID);
DLink current = first;
while(current.id!=key){
current=current.next;
if(current==null)
return false;
}
//first不包含任何信息所以这句话有错误
/*if(current==first){
current=newLink;
}*/
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 DLink deleteFirst(){
DLink temp=first;
if(isEmpty())
return null;
else{
first=temp.next;
temp.next.previous=null;
}
return temp;
}
//删除最后一个链结点
public DLink deleteLast(){
DLink temp = last;
if(first.next==null)
first=null;
else
last.previous.next=null;
last=last.previous;
return temp;
}
//删除特定链结点
public DLink deleteID(int key){
DLink current = first;
while(current.id!=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 displayDoubleLinkForward(){
System.out.println("display DoubleLink :");
DLink current = first;
while(current!=null){
current.displayDLink();
current = current.next;
}
System.out.println(" ....end ");
}
public void displayDoubleLinkBackward(){
System.out.println("display DoubleLink :");
DLink current = last;
while(current!=null){
current.displayDLink();
current = current.previous;
}
System.out.println(" ....end ");
}
}
public class DoubleLinkApp {
public static void main(String[] args) {
DoubleLinkList doubleLinkList = new DoubleLinkList();
doubleLinkList.insertFirst(22);
doubleLinkList.insertFirst(24);
doubleLinkList.insertFirst(34);
doubleLinkList.insertFirst(56);
doubleLinkList.insertFirst(87);
doubleLinkList.displayDoubleLinkBackward();
doubleLinkList.displayDoubleLinkForward();
doubleLinkList.deleteFirst();
doubleLinkList.displayDoubleLinkForward();
doubleLinkList.deleteLast();
doubleLinkList.displayDoubleLinkBackward();
doubleLinkList.insertAfter(34, 99);
doubleLinkList.displayDoubleLinkForward();
}
}
除非链表是空的,否则insertFirst()方法把原先first指向的链结点的previous字段指向新链结点,把新链结点的next字段指向前者。最后把first指向新链结点。如下图所示:
如果链表是空的,last字段必须改变,而不是first.previous字段改变,下面是代码:
if (isEmpty())
last = newLink;
else
first.previous = newLink;
//previous默认是null 所以无需注明newLink.previous=null
newLink.next = first;
first = newLink;
insertAfter()方法在某个特定值的链结点后插入一个新的链结点。首先,必须要先找到特定值的链结点,然后假设插入点不在表尾,首先建立新链结点和下一个链结点之间的两个连接,接着是建立current所指链结点和新链结点之间的两个连接,如下图所示:
如果新链结点插在表尾,它的next字段必须设为null值,last值必须指向新链结点。
//在特定的节点后面插入, key为特定节点,insertID为插入的节点
public boolean insertAfter(int key,int insertID){
DLink newLink = new DLink(insertID);
DLink current = first;
while(current.id!=key){
current=current.next;
if(current==null)
return false;
}
//first不包含任何信息所以这句话有错误
/*if(current==first){
current=newLink;
}*/
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;
}
删除
在deleteKey()方法中,被删除的关键值链结点是current所指链结点。假设被删的链结点既不是第一个链结点,也不是最后一个,current.previous(被删链结点的前一个链结点)的next字段指向current.next(被删链结点的后一个链结点),current.next的previous字段指向current.previous。这样就使current指向的链结点和链表断开了连接。
//先找到需要删除的链结点
while(current.id!=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;
参考资料:java数据结构和算法(第二版)