A: 数组做为数据存储结构有必定的缺陷,在无序数组中,搜索是低效的;而在有序数组中,插入效率又很低;无论在哪个数组中删除效率都很低;何况一个数组建立后,它的大小是不可改变的。java
A: 在本篇中,咱们将学习一种新的数据结构 —— 链表,它能够解决上面的一些问题,链表多是继数组以后第二种使用最普遍的通用存储结构了。git
A: 在链表中,每一个数据项都被包含在“结点”中,可使用Node, 或者Entry等名词来表示结点,本篇使用Entry来表示。每一个Entry对象中包含一个对下一个结点引用的字段(一般叫作next),单链表中每一个结点的结构图以下:
定义单链表结点的类定义以下:程序员
class Entry<E> { E mElement; Entry<E> mNext; public Entry(E element, Entry<E> next) { mElement = element; mNext = next; } }
A: 构成链表的结点只有一个指向后继结点的指针域。算法
A: 示例:SingleLinkedList.java数组
A: LinkedList类只包含一个数据项mHeader,叫作表头:即对链表中第一个节点的引用。它是惟一的链表须要维护的永久信息,用以定位全部其余的连接点。从mHeader出发,沿着链表经过每一个结点的mNext字段,就能够找到其余的结点。数据结构
A: addFirst()方法 —— 做用是在表头插入一个新结点。 学习
A: removeFirst()方法 —— 是addFirst()方法的逆操做,它经过把mHeader从新指向第二个结点,断开了和第一个结点的链接。
在C++中,从链表取下一个结点后,须要考虑如何释放这个结点。它仍然在内存中的某个地方,可是如今没有任何指针指向它,将如何处理它呢?在Java中,垃圾回收(GC)将在将来的某个时刻销毁它,如今这不是程序员操心的工做。
注意,removeFirst()方法假定链表不是空的,所以调用它以前,应该首先调用empty()方法核实这一点。测试
A: indexOf(Object)方法 —— 返回此列表中首次出现的指定元素的索引,若是此列表中不包含该元素,则返回 -1。spa
get(int)方法 —— 返回此列表中指定位置处的元素。设计
A: remove(Object) —— 今后列表中移除首次出现的指定元素(若是存在)。
先搜索要删除的结点,若是找到了,必须把前一个结点和后一个结点连起来,知道前一个结点的惟一方法就是拥有一个对它的引用previous(每当current变量赋值为current.next以前,先把previous变量赋值为current)。
A: 示例: SingleLinkedList.java
A: 双端链表(double-ended list )是在上边的单链表基础上加了一个表尾,即对最后一个结点的引用。以下图:
A: 对最后一个结点的引用容许像表头同样,在表尾直接插入一个结点。固然,仍然能够在普通的单链表的表尾插入一个结点,方法是遍历整个链表直到到达表尾,可是这种方法效率很低。
A: 示例: DoubleEndedList.java
A: DoubleEndedList有两个项,header和tailer,一个指向链表中的第一个结点,另外一个指向最后一个结点。
A: 若是链表中只有一个结点,header和last都指向它。若是没有结点,两个都为null值。
A: 若是链表只有一个结点,删除时tailer必须被赋值为null。
A: addLast()方法 —— 在表尾插入一个新结点。
A: 在表头插入和删除速度很快,仅须要改变一两个引用值,因此花费O(1)的时间。
A: 查找、删除和在指定结点的前面插入都须要搜索链表中一半的结点,须要O(N)次比较,在数组中执行这些操做也须要O(N)次比较。可是链表仍然要快一些,由于插入和删除结点时,链表不须要移动任何东西。
A: 链表比数组还有一个优势是,链表须要多少内存就能够用多少内存,不像数组在建立时大小就固定了。
A: 向量是一种可扩展的数组,它能够经过可变长度解决这个问题,可是它常常只容许以固定的增量扩展(好比快要溢出的时候,就增长1倍的数组容量)。这个解决方案在内存使用效率上来讲仍是要比链表低。
A: 示例:Stack.java
A: 栈的使用者不须要知道栈用的是链表仍是数组实现。 所以Stack类的测试用例在这两个上是没有分别的。
A: 示例:Queue.java
A: 展现了一个用双端链表实现的队列。
A: 这一点要取决因而否能精准地预测栈或队列须要容纳的数据量。若是这一点不是很清楚的话,链表就比数组表现出更好的适用性。二者都很快,因此速度可能不是考虑的重点。
A: 简单来讲,它是一种考虑数据结构的方式:着重于它作了什么,而忽略它是怎么作的。
A: 栈和队列都是ADT的例子,前面已经看到栈和队列既能够用数组实现,也可使用链表实现,而对于使用它们的用户彻底不知道具体的实现细节(用户不只不知道方法是怎样运行,也不知道数据是如何存储的)。
A: ADT的概念在软件设计过程当中很重要,若是须要存储数据,那么就要从它的实际操做上开始考虑,好比,是存取最后一个插入的数据项?仍是第一个?是特定值的项?仍是在特定位置上的项?回答这些问题会引出ADT的定义。
A: 只有在完整定义ADT后,才应该考虑细节问题。
A: 经过从ADT规范中剔除实现的细节,能够简化设计过程,在将来的某个时刻,易于修改实现。若是用户只接触ADT接口,应该能够在不“干扰”用户代码的状况下修改接口的实现。
A: 固然,一旦设计好ADT,必须仔细选择内部的数据结构,以使规定的操做的效率尽量高。例如随机存取元素a,那么用链表表示就不够好,由于对链表来讲,随机访问不是一个高效的操做,选择数据会获得更好的效果。
A: 在有序链表中,数据是按照关键值有序排列的,有序链表的删除经常是只限于删除在表头的最小(或最大)的节点。
A: 通常,在大多数须要使用有序数组的场合也可使用有序链表。有序链表的优点在于插入的速度,由于元素不须要移动,并且链表能够随时扩展所需内存,数组只能局限于一个固定大小的内存。
A: 示例:SortedLinkedList.java
A: 当算法找到要插入的位置,用一般的方式插入数据项:把新节点的next字段指向下一个节点,而后把前一个结点的next字段指向新节点。然而,须要考虑一些特殊状况:节点有可能插在表头,或者表尾。
A: 在有序链表插入或删除某一项最多须要O(N)次比较(平均N/2),由于必须沿着链表一步一步走才能找到正确的位置。然而,能够在O(1)的时间内找到或删除最小值,由于它总在表头。
A: 若是一个应用频繁地存取最小项,且不须要快速地插入,那么有序链表是一个有效的方案选择,例如,优先级队列能够用有序链表来实现。
A: 有序链表能够用于一种高效的排序机制。假设有一个无序数组,若是从这个数组中取出数据,而后一个一个地插入有序链表,它们自动地按照顺序排列。而后把它们从有序链表删除,从新放入数组,那么数组就排好序了。
A: 本质上与基于数组的插入排序是同样的,都是O(N2)的比较次数,只是说对于数组会有一半已存在的数据会涉及移动,至关于N2/4次移动,相比之下,链表只需2 * N次移动:一次是从数组到链表,一次是从链表到数组。
A: 不过链表插入有一个缺点:就是它要开辟差很少两倍的空间。
A: 示例: LinkedListSort.java
A: 双向链表提供了这样的能力,即容许向前遍历,也容许向后遍历整个链表,其中秘密在于它的每一个数据结点中都有两个指针,分别指向直接后继和直接前驱。
A: 双向链表没必要是双端链表(保持一个对链表最后一个元素的引用),但这种方式是颇有用的。因此下面的示例将包含双端的性质。
A: 示例:DoublyLinkedList.java
A: addFirst(E)方法:将指定元素插入此列表的开头。
A: addLast(E)方法:将指定元素添加到此列表的结尾。
A: add(index, E)方法: 在此列表中指定的位置插入指定的元素。
A: remove(Object o)方法: 今后列表中移除首次出现的指定元素(若是存在)。
A: 双向链表能够用来做为双端队列的基础。在双端队列中,能够从任何一头插入和删除,双向链表提供了这个能力。
A: ArrayList底层维护的是一个数组;LinkedList是链表结构的;HashSet依赖的是哈希表,每种容器都有本身特有的数据结构。由于容器的内部结构不一样,不少时候可能不知道该怎样去遍历一个容器中的元素。因此为了使对容器内元素的操做更为简单,Java引入了迭代器。
A: 把访问逻辑从不一样类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。
对于数组咱们使用的是下标来进行处理的:
for (int i = 0; i < array.length; i++) { System.out.println(array[i]); }
对于链表,咱们从表头开始遍历:
public void displayForward() { System.out.print("List (first-->last): ["); Entry<E> current = mHeader; while(current != null) { E e = current.mElement; System.out.print(e); if (current.mNext != null) { System.out.print(" "); } current = current.mNext; } System.out.print("]\n"); }
A: 不一样的集合会对应不一样的遍历方法,客户端代码没法复用。在实际应用中如何将上面两个集合整合是至关麻烦的。因此才有Iterator,它老是用同一种逻辑来遍历集合。使得客户端自身不须要来维护集合的内部结构,全部的内部状态都由Iterator来维护。客户端不用直接和集合进行打交道,而是控制Iterator向它发送向前向后的指令,就能够遍历集合。
A: 迭代器模式就是提供一种方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节。
A: 迭代器包含对数据结构中数据项的引用,而且用来遍历这些结构的对象。下面是迭代器的接口定义:
public interface Iterator<E> { boolean hasNext(); E next(); void remove(); }
public interface ListIterator<E> extends Iterator<E> { boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void set(E e); }
A: 每一个容器的iterator()方法返回一个标准的Iterator实现。通常而言,Java中迭代器和链表以前的链接是经过把迭代器设为链表的内部类来实现,而C++是"友元"来实现。
A: 以下图显示了指向链表的某个结点的两个迭代器:
A: 迭代器类ListItr实现ListIterator接口,定义以下:
private class ListItr implements ListIterator<E> { }
A: 示例:ListIteratorTestCase.java
A: 迭代器类的一个设计问题是决定在不一样的操做后,迭代器应该指向哪里。而JDK1.6中LinkedList.ListItr中的add()实现,next指针一直指向表头,这里假设调用的是iterator(),不指定下标。