链表(Java描述)

来源:互联网 发布:ug钻孔编程后处理 编辑:程序博客网 时间:2024/06/06 11:10

原文地址: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<AnyType>{   private AnyType data;   private Node<AnyType> next;   public Node(AnyType data, Node<AnyType> 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<AnyType>(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<AnyType> tmp = head;      while(tmp.next != null) tmp = tmp.next;      tmp.next = new Node<AnyType>(item, null);   }}

结点后插入

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

public void insertAfter(AnyType key, AnyType toInsert){   Node<AnyType> tmp = head;   while(tmp != null && !tmp.data.equals(key)) tmp = tmp.next;   if(tmp != null)      tmp.next = new Node<AnyType>(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<AnyType> prev = null;   Node<AnyType> 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<AnyType>(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<AnyType> cur  = head;   Node<AnyType> 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接口包含下列方法:

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

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

public Iterator<AnyType> iterator(){   return new LinkedListIterator();}

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

private class LinkedListIterator implements Iterator<AnyType>{   private Node<AnyType> 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<AnyType> twin = new LinkedList<AnyType>();   Node<AnyType> tmp = head;   while(tmp != null)   {      twin.addFirst( tmp.data );      tmp = tmp.next;   }   return twin.reverse();}

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

public LinkedList<AnyType> copy3(){   if(head==null) return null;   LinkedList<AnyType> twin = new LinkedList<AnyType>();   Node tmp = head;   twin.head = new Node<AnyType>(head.data, null);   Node tmpTwin = twin.head;   while(tmp.next != null)   {      tmp = tmp.next;      tmpTwin.next = new Node<AnyType>(tmp.data, null);      tmpTwin = tmpTwin.next;   }   return twin;}

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


        感谢阅读!
0 0