链表(Java描述)

引言

        使用数组来存储数据的一个缺点在于,数组是静态结构,因此不能很容易地扩展或缩小以适合数据集。对数组进行插入和删除的代价也比较高。本文考虑一种名为链表的数据结构,解决了数组的一些局限性。

        链表是一种线性数据结构,每个元素都是一个单独的对象。

        链表的每个元素(称之为一个节点)包括两个部分—数据域和指针域(链接到下一个节点)。最后一个节点链接到null。链表的起点称为头结点。应该注意的是,头结点不是一个单独的节点,而是链接到第一个节点。如果链表为空,则头结点为空引用。

        链表是一种动态数据结构。一个链表中的节点数不是固定的,可按需增多和减少。任何要处理未知数目对象的应用需要使用链表。

        链表的一个缺点是它不允许直接访问各个元素。如果要访问一个特定的元素,必须从头结点开始,沿着链接,直到得到该元素。

        与数组相比,链表的另一个缺点是它占用更多的内存—需要额外的4个字节(32位CPU)来存储到下一个节点的引用。

链表的类型

        单链表如上所述。

        双向链表有两个引用,一个链接到下一个节点,另一个链接到前面的节点。

        链表的另一种重要类型称为循环链表,其中链表的最后一个节点指向第一个节点(或头结点)。

结点类

        在Java中,可以在一个类(例如A)的内部定义另一个类(例如B)。A类称为外部类,B类称为内部类。内部类的目的纯粹是为了在内部作为辅助类。下面是具有内部节点类的LinkedList类:

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">private static class Node<AnyType>  
  2. {  
  3.    private AnyType data;  
  4.    private Node<AnyType> next;  
  5.   
  6.    public Node(AnyType data, Node<AnyType> next)  
  7.    {  
  8.       this.data = data;  
  9.       this.next = next;  
  10.    }  
  11. }</span>  

        一个内部类是它的外部类的成员,并可访问外部类的其他成员(包括私有成员),反之亦然,即外部类可以直接访问内部类的所有成员。一个内部类可以声明为私有,公共,保护或包权限。有两种类型的内部类:静态的和非静态的。静态内部类不能直接引用实例变量或在其外部类中定义的方法,它只能通过对象引用来使用它们。

        我们用两个内部类来实现LinkedList类:静态节点类和非静态LinkedListIterator类。完整的实现参看LinkedList.java

实例

        对于上面的单链表,跟踪各片段,效果如下。在每一行执行之前链表恢复到其初始状态。

1.  head = head.next;

2.  head.next = head.next.next;

3.  head.next.next.next.next= head;

链表操作

addFirst

        该方法创建一个节点,并把它加在链表的开头。

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public void addFirst(AnyType item)  
  2. {  
  3.    head = new Node<AnyType>(item, head);  
  4. }  
  5. </span>  

遍历

        从头结点开始,访问每个节点,直到到达null。不要更改头结点。

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">Node tmp = head;  
  2.   
  3. while(tmp != null) tmp = tmp.next;</span>  

addLast

        该方法将节点加到链表的末尾。这需要遍历,但要确保停在最后一个节点。

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public void addLast(AnyType item)  
  2. {  
  3.    if(head == null) addFirst(item);  
  4.    else  
  5.    {  
  6.       Node<AnyType> tmp = head;  
  7.       while(tmp.next != null) tmp = tmp.next;  
  8.   
  9.       tmp.next = new Node<AnyType>(item, null);  
  10.    }  
  11. }  
  12. </span>  

结点后插入

        查找包含“key”的节点,在其后插入一个新的节点。在下图中,我们在“E”之后插入一个新的节点:

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public void insertAfter(AnyType key, AnyType toInsert)  
  2. {  
  3.    Node<AnyType> tmp = head;  
  4.    while(tmp != null && !tmp.data.equals(key)) tmp = tmp.next;  
  5.   
  6.    if(tmp != null)  
  7.       tmp.next = new Node<AnyType>(toInsert, tmp.next);  
  8. }  
  9. </span>  

结点前插入

        查找包含“key”的节点,在该节点之前插入一个新的节点。在下图中,我们在“A”之前插入一个新的节点:

        为方便起见,维护两个引用(reference)prev 和cur。沿着链表移动,移动这两个引用,保持prev在cur的前一步。继续下去,直到cur到达需要在其之前插入的节点。如果cur到达null,我们不插入,否则在prev和cur之间插入一个新的节点。

        观察这个实现:

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public void insertBefore(AnyType key, AnyType toInsert)  
  2. {  
  3.    if(head == nullreturn null;  
  4.    if(head.data.equals(key))  
  5.    {  
  6.       addFirst(toInsert);  
  7.       return;  
  8.    }  
  9.   
  10.    Node<AnyType> prev = null;  
  11.    Node<AnyType> cur = head;  
  12.   
  13.    while(cur != null && !cur.data.equals(key))  
  14.    {  
  15.       prev = cur;  
  16.       cur = cur.next;  
  17.    }  
  18.    //insert between cur and prev  
  19.    if(cur != null) prev.next = new Node<AnyType>(toInsert, cur);  
  20. }  
  21. </span>  

删除

        查找包含"key"的节点,并删除它。在下图中我们删除包含“A”的节点。

        该算法类似于在结点前插入的算法。使用两个引用prev和cur较为方便。沿着链表移动,移动这两个引用,保持prev在cur的前一步。继续下去,直到cur到达我们需要删除的节点。我们需要考虑三种特殊情况:

1 链表为空

2 删除头节点

3 节点不在链表中

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public void remove(AnyType key)  
  2. {  
  3.    if(head == nullthrow new RuntimeException("cannot delete");  
  4.   
  5.    if( head.data.equals(key) )  
  6.    {  
  7.       head = head.next;  
  8.       return;  
  9.    }  
  10.   
  11.    Node<AnyType> cur  = head;  
  12.    Node<AnyType> prev = null;  
  13.   
  14.    while(cur != null && !cur.data.equals(key) )  
  15.    {  
  16.       prev = cur;  
  17.       cur = cur.next;  
  18.    }  
  19.   
  20.    if(cur == nullthrow new RuntimeException("cannot delete");  
  21.   
  22.    //delete cur node  
  23.    prev.next = cur.next;  
  24. }  
  25. </span>  

迭代器

        迭代器的思想是提供访问私有聚合数据的方式,同时隐藏底层表示。Java迭代器是一个对象,因此它的实现需要创建一个实现了Iterator 接口的类。通常这样的类被实现为一个私有内部类。该Iterator接口包含下列方法:

  • AnyType next() - 返回容器中的下一个元素
  • boolean hasNext() - 检查是否存在下一个元素
  • void remove() -(可选操作)。删除由next()返回的元素

        在本节中,我们在LinkedList类中实现了Iterator。首先在LinkedList类中添加一个新的方法:

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public Iterator<AnyType> iterator()  
  2. {  
  3.    return new LinkedListIterator();  
  4. }  
  5. </span>  

        这里LinkedListIterator 是LinkedList类中的私有类。

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">private class LinkedListIterator implements Iterator<AnyType>  
  2. {  
  3.    private Node<AnyType> nextNode;  
  4.   
  5.    public LinkedListIterator()  
  6.    {  
  7.       nextNode = head;  
  8.    }  
  9.    ...  
  10. }  
  11. </span>  

        该LinkedListIterator类必须提供next()和hasNext()方法的实现。next()方法如下:

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public AnyType next()  
  2. {  
  3.    if(!hasNext()) throw new NoSuchElementException();  
  4.    AnyType res = nextNode.data;  
  5.    nextNode = nextNode.next;  
  6.    return res;  
  7. }  
  8. </span>  

复制(Cloning

        像任何其他对象一样,我们需要学习如何复制链表。如果简单地使用Object类的clone()方法,我们会得到一个名为浅拷贝的结构:

        Object类的clone()将创建第一个节点的副本,并共享其他结点。这并不是我们所说的“该对象的副本”。其实我们要的是下图所代表的副本:

        由于数据是不可变的,可以在两个链表之间共享数据。有几种方法来实现链表复制。最简单的一种是遍历原始链表,并使用addFirst()方法复制每个节点。这个完成后,将得到一个具有相反顺序的新链表。最后,我们一定要反转链表:

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public Object copy()  
  2. {  
  3.    LinkedList<AnyType> twin = new LinkedList<AnyType>();  
  4.    Node<AnyType> tmp = head;  
  5.    while(tmp != null)  
  6.    {  
  7.       twin.addFirst( tmp.data );  
  8.       tmp = tmp.next;  
  9.    }  
  10.   
  11.    return twin.reverse();  
  12. }  
  13. </span>  

        一个更好的方法是为新链表使用一个尾引用,在最后一个节点之后添加每个新节点。

[java]   view plain  copy   在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:18px;">public LinkedList<AnyType> copy3()  
  2. {  
  3.    if(head==nullreturn null;  
  4.    LinkedList<AnyType> twin = new LinkedList<AnyType>();  
  5.    Node tmp = head;  
  6.    twin.head = new Node<AnyType>(head.data, null);  
  7.    Node tmpTwin = twin.head;  
  8.   
  9.    while(tmp.next != null)  
  10.    {  
  11.       tmp = tmp.next;  
  12.       tmpTwin.next = new Node<AnyType>(tmp.data, null);  
  13.       tmpTwin = tmpTwin.next;  
  14.    }  
  15.   
  16.    return twin;  
  17. }  
  18. </span>  

        (多项式代数应用部分未翻译)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值