链表

链表的机制灵活,用途广泛,它适用于许多通用的数据库,它也可以取代数组,作为其他存储结构的基础。

1 链接点

在链表中,每个数据项都包含在“链接点(Link)中”。一个链接点是某个类的对象,这个类可以叫做Link。因为一个中有许多类似的链接点,所以有必要用一个不同于链表的类来表达链接点。每个Link对象中都包含一个对下一个链接点引用的字段(通常叫做next)。但是链表本身的对象中有一个字段指向对第一个链接点的引用。

下面是一个Link类定义的一部分。它包含了一些数据和对下一个链接点的引用:

class Link{
    public int iData;       //链接点数据1
    public double dData;    //链接点数据2
    public Link next;              //对下一个链接点的引用
}

这种类定义有时叫做“自引用”式,因为它包含了一个和自己类型相同的字段。

2 单链表

单链表仅有的操作:

  1. 在链表透插入一个数据项;
  2. 在链表头删除一个数据项;
  3. 遍历链表显示它的内容;

程序实现:
Link类定义的完整实现:

class Link{
    public int iData;
    public double dData;
    public Link next;
    public Link(int iData, double dData){
        this.iData = iData;
        this.dData = dData;
    }
    public void displayLink(){
        System.out.println("{" + iData + ", " + dData + "}")
    }
}

构造函数初始化数据,但是不需要初始化next字段,因为当它被创建时自动赋值为null。

LinkList类

LinkList类中只包含一个数据项:即对链表中第一个链接点的引用,叫做first。它是唯一的链表需要维护的永久信息,用以定位所有其他的链接点。从first出发,沿着链表通过每个链接点的next字段,就可以找到其他的链接点:

public class LinkList {
    private Link first;
    public LinkList(){
        first = null;
    }
    
    public boolean isEmpty(){
        return first == null;
    }
}

LinkList链表创建的时候,里面是没有数据的,所以first指向的是null。isEmpty用来判断链表是否为空。

insertFirst()方法

insertFirst()的作用是在表头插入一个新的链接点。因为first已经指向了第一个链接点,为了插入新的链接点,需要使新创建的链接点的next字段指向当前的first链接点,链表中的first字段指向新创建的链接点。

public void insertFirst(int iData, double dData){
    Link newLink = new Link(iData, dData);   //创建新的链接点
    newLink.next = first;               //新的链接点的next指向原来链表中的第一个元素
    first = newLink;                    //first指向新创建的元素
}

deleteFirst()方法

这个方法是insertFirst方法的逆操作,断开第一个链接点,使第一个链接点指向第二个链接点。

public Link deleteFirst(){
    Link temp = first;
    if (first != null){             //防止链表为空,也可以通过isEmpty方法判断
        first = first.next; 
    }
    return temp;
}

在Java语言中,垃圾收集进程将在未来的某个时刻销毁它。

displayList()方法

为了显示链表,从first开始,沿着引用链从一个链接点到下一个链接点。

public void displayLink(){
    Link current = first;
    while (current != null){    //输出所有的连接点数据,一直到连接点为空
        current.displayLink();  //打印当前的链接点
        current = current.next; //当前的链接点打印完毕,指向当前连接点的下一个
    }
    System.out.println();       //打印换行符
}

public class LinkListTest {
    @Test
    public void testLinkList(){
        LinkList linkList = new LinkList();
        linkList.insertFirst(1, 1.1);
        linkList.insertFirst(2, 2.2);
        linkList.insertFirst(3, 3.3);
        linkList.displayLink();
        linkList.deleteFirst();
        linkList.displayLink();
    }
}

输出结果:

{3, 3.3}
{2, 2.2}
{1, 1.1}

{2, 2.2}
{1, 1.1}

查找和删除链接点

下面的代码中增加了两个方法,一个是查找含有特定关键字的链接点,另外一个是删除含有特定关键字的链接点。

find()方法从first元素开始,遍历linklist的列表找到含有关键字key的链接点。

public Link find(int key){
    if(isEmpty()){          //如果链表为空直接返回null
        return null;
    }
    Link current = first;
    while (current != null){
        if(current.iData == key){   //判断当前链接点是否包含给定的关键字
            return current;
        }
        current = current.next;
    }
    return null;
}

delete()方法首先和find()方法一样遍历linklist找到含有关键字key的链接点,然后让找到的链接点的前一个链接点的next,指向此链接点的后一个链接点。这也是为什么代码中要用一个previous变量记录当前连接点的前一个链接点的原因。

public Link delete(int key){
    if(isEmpty()){
        return null;
    }
    Link current = first;
    Link previous = current;
    while (current.iData != key){
        if(current.next == null){
            return null;
        }
        previous = current;
        current = current.next;
    }
    if(current.equals(first)){
        first = current.next;
    }else {
        previous.next = current.next;
    }
    return current;
}

下面来测试这两个方法:
@Test
public void testFind(){
LinkList linkList = new LinkList();
linkList.insertFirst(1, 1.1);
linkList.insertFirst(2, 2.2);
linkList.insertFirst(3, 3.3);

    Link link = linkList.find(3);
    Assert.assertTrue(link.iData == 3);
    Assert.assertTrue(link.dData == 3.3);

    link = linkList.find(5);
    Assert.assertNull(link);
}

@Test
public void testDelete(){
    LinkList linkList = new LinkList();
    linkList.insertFirst(1, 1.1);
    linkList.insertFirst(2, 2.2);
    linkList.insertFirst(3, 3.3);
    Link link = linkList.delete(3);
    Assert.assertTrue(link.iData == 3);
    Assert.assertTrue(link.dData == 3.3);
    linkList.displayLink();

    linkList.delete(1);
    linkList.displayLink();
}

testDelete()的输出结果为:
{2, 2.2}

{2, 2.2}

3 双端链表

双端链表与传统的链表非常类似,但是它有一个新特性:即对最后一个连接点的引用,就像对第一个链接点的引用一样。

对最后一个链接点的引用允许像在表头一样,在表尾直接插入一个链接点。当然,仍然可以在普通的单链表的表尾插入一个链接点,方法是遍历整个链表直到到达表尾,但是这种方法很低。

下面的代码表示了一个双端链表:

public class FirstLastList {
    public Link first;      //指向链表中的第一个元素
    public Link last;       //指向链表中的最后一个元素
    
    public FirstLastList(){     //初始化链表,链表当前是空的,所以first和last都是空
        first = null;
        last = null;
    }
    
    public boolean isEmpty(){
        return  first == null;
    }
    
    public void insertFirst(int iData, double dData){
        Link link = new Link(iData, dData);
        if(isEmpty()){          //链表是空的情况下,last是没有指向的,添加一个链接点后last指向当前添加的链接点
            last = link;
        }
        link.next = first;     //新添加链接点的next指向,第一个链接点(first)的next
        first = link;           //第一个链接点(first)指向当前添加的链接点
    }
    
    public void insertLast(int iData, double dData){
        Link link = new Link(iData, dData);
        if(isEmpty()){          //链表为空的情况下,first没有指向,在链表的结尾添加一个元素使first执行新添加的元素
            first = link;
        }else {
            last.next = link;    //链表最后一个链接点指向当前新创建的链接点
            last = link;         //并且,链表last也指向当前新创建的链接点
        }
    }
    
    public Link deleteFirst(){
        if(isEmpty()){          //链表为空的情况下,没有元素可以被删除
            return null;
        }
        Link temp = first;      //把要删除的链接点,放在一个临时变量里面,以备后面return用
        first = first.next;     //把第一个链接点指向其的next
        if(first == null){      //如果链表中只有一个链接点,删除其后,last的指向也为空
            last = null;
        }
        return temp;
    }
    
    public void disaplyLink(){
        System.out.println("first-->last:");
        Link current = first;
        while (current != null){        //遍历链表,输出所有元素
            current.displayLink();
            current = current.next;
        }
        System.out.println("");  //换行
    }
}

测试双端链表:

@Test
public void testFirstLastList(){
     FirstLastList firstLastList = new FirstLastList();  //新建一个双端链表
    Assert.assertTrue(firstLastList.isEmpty());     //判断当前新建的链表为空
    firstLastList.insertFirst(1, 1.1);      //在链表的头部添加一个链接点
    firstLastList.insertFirst(2, 2.2);      //在链表的头部添加第二个链接点
    firstLastList.insertLast(3, 3.3);       //在链表的尾部添加一个链接点
    firstLastList.displayLink();
}

输出结果:

first-->last:
{2, 2.2}
{1, 1.1}
{3, 3.3}

注意:在链表的头部插入的元素会颠倒输出的顺序,而在表尾的重复插入则保持链接点进入的顺序。

在链表的尾部插入元素的示意图:

链表的效率

在链表的头部插入和删除速度很快,仅需改变一两个引用值,所以花费O(1)的时间。

平均起来,查找,删除和在指定链接点后面插入都需要搜索链表中的一半链接点。需要O(N)次比较。在数组中执行这些操作也需要O(N)次比较,但是链表仍然要快些,因为当插入和删除链接点时,不需要移动任何东西。增加的效率是很显著的,特别是当复制时间远大于比较时间的时候。

当然,链表比数组优越的另一个方面是链表需要多少内存就可以用多少内存,并且可以扩展到所有孔用内存。数组的大小在它创建的时候就固定了。所以经常由于数组太大导致效率低下或者数组太小导致空间溢出。

有序链表

在有序链表中,数据是按照关键值有序排列的。有序链表的删除常常是只限于删除在链表头部的链接点。

在有序链表中插入一个数据项(链接点)

为了在一个有序链表中插入数据项,算法必须首先搜索链表,直到找到合适的位置。当算法找到了合适的位置,用通常的方式插入数据项:把新的链接点的next字段指向下一个链接点,然后把前一个链接点的next字段指向当前插入的链接点。然而,有一些特殊情况需要考虑:链接点有可能在链表的头部,或者链表的尾部。

(为了简化代码,我们按照iData的大小进行演示)

public class SortedLink {
    public Link first;
    public SortedLink(){
        first = null;
    }
    
    public boolean isEmpty(){
        return first == null;
    }
    
    public void insert(int iData, double dData){
        Link link = new Link(iData, dData);
        Link previous = null;
        Link current = first;
        while (current != null && iData > current.iData){  //循环链表找到合适的位置插入链接点
            previous = current;
            current = current.next;
        }
        if(previous == null){   //如果previous是null,说明当前链表是空的,使first指向新增加的链接点即可
            first = link;
        }else {                 //链表不是空的情况下
            previous.next = link;
            link.next = current;
        }
    }
}

有序链表的效率

在有序链表插入和删除某一项最多需要O(N)次比较,因为必须沿着链表一步一步比较才能找到合适的插入位置。然而,可以在O(1)内找到或删除最小值,因为它总在链表的头部。如果一个应用频繁的存取最小值,且不需要快速的插入,那么有序链表是一个有效的方案。

4 双向链表

传统链表的一个潜在问题是沿链表的反向遍历是困难的。使用current=current.next可以很方便地到达下一个链接点,然而没有对应的方法回到前一个链接点。
双向链表提供了这个能力。即允许向前遍历,也允许向后遍历整个链表。其中秘密在于每个链接点有两个指向其他链接点的引用,而不是一个。第一个像普通链表一样指向下一个链接点。第二个指向前一个链接点。

双向链表的结构:

public class DoubleLink {
    public long data;
    public DoubleLink next;
    public DoubleLink pervious;
}

双向链表的缺点是每次插入或删除链接点的时候,要处理四个链接点的引用,而不是两个:两个连接前一个链接点,两个连接后一个链接点。

遍历

双向链表中有两个遍历方法,一个是displayForward(),另外一个displayBackward(), displayForward()和上面提到的displayList一样。displayBackward()方法与他们类似,但是是从链表的尾部开始,通过每一个链接点的pervious遍历,一步一步到达链表的头部。

public void displayBackward(){
    DoubleLink current = last;
    while (current != null){
        current.displayLink();
        current = current.pervious;
    }
}

插入

在下面的双向链表示例中,包含了几个插入方法。insertFirst()链表头部插入元素, insertLast()链表尾部插入元素, insertAfter()在某个元素的后面插入元素。

除非这个链表是空的,否则insertFirst()方法把原先first指向的链接点的pervious指向当前新插入的元素,把新插入的元素的next指向原先first指向的链接点,最后把first指向当前插入的元素。

public void insertFirst(long data){
    DoubleLink newLink = new DoubleLink(data);
    if(isEmpty()){              //如果链表是空的,last,first同时指向当前插入的链接点
        first = newLink;
        last = newLink;
        return;
    }
    //链表不是空的情况下
    first.pervious = newLink;   //原来first指向的链接点的previous指向新添加的链接点
    newLink.next = first;       //新添加的链接点指向原来的first
    first = newLink;            //first指向新添加的链接点
}

insertlast是同样的方法。只不过是在链表的尾部:它和insertFirst()的方法呈镜像。

insertAfter()方法在某个特定值的链接点后插入一个新的链接点。它有点复杂,因为需要建立四个连接。
1.找到具有特定值的连接点(和find方法类似)

2.假设找到的连接点不在链表的头部和尾部,首先需要建立新的链接点和下一个链接点之间的链接。接着建立当前查找到的current链接点和新建的链接点之间的链接。

3.如果查找到的链接点在链表尾部,它的next字段必须设为null值,last值必须指向新建的链接点。

public void insertAfter(long key, long insertValue){
    DoubleLink newLink = new DoubleLink(insertValue);
    if(isEmpty()){          //链表是空的情况下,相当于直接插入了一个链接点,把first和last的指针指向这个链接点即可
        first = newLink;
        last = newLink;
        return;
    }
    DoubleLink current = first;
    //循环列表找到和key匹配的链接点,能找到的话,保存在current变量里面,找不到的话,current是null
    while (current != null && current.data != key){
        current = current.next;
    }
    //如果没有找到,在链表的尾部插入一个连接点
    //如果找到的连接点正好是链表的尾部,同样在尾部插入一个连接点
    if(current == null || current == last ){
        last.next = newLink;
        newLink.pervious = last;
        last = newLink;
    }else {
        //其他都是正常情况,相当于在链表的中间找到了key匹配的连接点,需要修改四个指针的指向
        newLink.next = current.next;
        current.next.pervious = newLink;
        current.next = newLink;
        newLink.pervious = current;
    }
}

测试用例:

@Test
public void testDoubleLinkTest(){
    DoubleLinkList linkList = new DoubleLinkList();
    linkList.insertFirst(1);
    linkList.insertFirst(2);
    linkList.insertFirst(3);
    linkList.insertFirst(4);
    linkList.displayForward();
    linkList.insertAfter(3, 5);
    linkList.displayForward();
    linkList.insertAfter(1, 6);
    linkList.displayForward();
    linkList.insertAfter(7, 8);
    linkList.displayForward();
}

输出结果:

4 ,3 ,2 ,1 ,
4 ,3 ,5 ,2 ,1 ,
4 ,3 ,5 ,2 ,1 ,6 ,
4 ,3 ,5 ,2 ,1 ,6 ,8 ,

完整代码下载请访问github代码库

posted @ 2016-05-21 12:36  王晓符  阅读(513)  评论(0编辑  收藏  举报
PS:如果你觉得文章对你有所帮助,别忘了推荐或者分享,因为有你的支持,才是我续写下篇的动力和源泉!
  • 作者: Greta Wang
    出处: http://www.cnblogs.com/greta/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。有事请联系邮箱wangqj541@163.com.