Q: 为什么要引入链表的概念?它是解决什么问题的?

A: 数组作为数据存储结构有一定的缺陷,在无序数组中,搜索是低效的;而在有序数组中,插入效率又很低;不管在哪一个数组中删除效率都很低;况且一个数组创建后,它的大小是不可改变的。

A: 在本篇中,我们将学习一种新的数据结构 —— 链表,它可以解决上面的一些问题,链表可能是继数组之后第二种使用最广泛的通用存储结构了。

Q: 结点?

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;
}
}

Q: 单链表?

A: 构成链表的结点只有一个指向后继结点的指针域。

Q: 单链表的Java实现?

A: 示例:SingleLinkedList.java

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

A: addFirst()方法 —— 作用是在表头插入一个新结点。

A: removeFirst()方法 —— 是addFirst()方法的逆操作,它通过把mHeader重新指向第二个结点,断开了和第一个结点的连接。 
 
在C++中,从链表取下一个结点后,需要考虑如何释放这个结点。它仍然在内存中的某个地方,但是现在没有任何指针指向它,将如何处理它呢?在Java中,垃圾回收(GC)将在未来的某个时刻销毁它,现在这不是程序员操心的工作。 
注意,removeFirst()方法假定链表不是空的,因此调用它之前,应该首先调用empty()方法核实这一点。

Q: 如何查找和删除指定的结点?

A: indexOf(Object)方法 —— 返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。

get(int)方法 —— 返回此列表中指定位置处的元素。

A: remove(Object) —— 从此列表中移除首次出现的指定元素(如果存在)。 
先搜索要删除的结点,如果找到了,必须把前一个结点和后一个结点连起来,知道前一个结点的唯一方法就是拥有一个对它的引用previous(每当current变量赋值为current.next之前,先把previous变量赋值为current)。 

A: 示例: SingleLinkedList.java

Q: 双端链表?

A: 双端链表(double-ended list )是在上边的单链表基础上加了一个表尾,即对最后一个结点的引用。如下图: 

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

Q: 双端链表的Java实现?

A: 示例: DoubleEndedList.java

A: DoubleEndedList有两个项,header和tailer,一个指向链表中的第一个结点,另一个指向最后一个结点。

A: 如果链表中只有一个结点,header和last都指向它。如果没有结点,两个都为null值。

A: 如果链表只有一个结点,删除时tailer必须被赋值为null。

A: addLast()方法 —— 在表尾插入一个新结点。

Q: 链表的效率?

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

A: 查找、删除和在指定结点的前面插入都需要搜索链表中一半的结点,需要O(N)次比较,在数组中执行这些操作也需要O(N)次比较。但是链表仍然要快一些,因为插入和删除结点时,链表不需要移动任何东西。

A: 链表比数组还有一个优点是,链表需要多少内存就可以用多少内存,不像数组在创建时大小就固定了。

A: 向量是一种可扩展的数组,它可以通过可变长度解决这个问题,但是它经常只允许以固定的增量扩展(比如快要溢出的时候,就增加1倍的数组容量)。这个解决方案在内存使用效率上来说还是要比链表低。

Q: 用链表实现的栈?

A: 示例:Stack.java

A: 栈的使用者不需要知道栈用的是链表还是数组实现。 因此Stack类的测试用例在这两个上是没有分别的。

Q: 用链表实现的队列?

A: 示例:Queue.java

A: 展示了一个用双端链表实现的队列。

Q: 什么时候应该使用链表而不是数组来实现栈和队列呢?

A: 这一点要取决于是否能精准地预测栈或队列需要容纳的数据量。如果这一点不是很清楚的话,链表就比数组表现出更好的适用性。两者都很快,所以速度可能不是考虑的重点。

Q: 什么是抽象数据类型(ADT)?

A: 简单来说,它是一种考虑数据结构的方式:着重于它做了什么,而忽略它是怎么做的。

A: 栈和队列都是ADT的例子,前面已经看到栈和队列既可以用数组实现,也可以使用链表实现,而对于使用它们的用户完全不知道具体的实现细节(用户不仅不知道方法是怎样运行,也不知道数据是如何存储的)。

A: ADT的概念在软件设计过程中很重要,如果需要存储数据,那么就要从它的实际操作上开始考虑,比如,是存取最后一个插入的数据项?还是第一个?是特定值的项?还是在特定位置上的项?回答这些问题会引出ADT的定义。

A: 只有在完整定义ADT后,才应该考虑细节问题。

A: 通过从ADT规范中剔除实现的细节,可以简化设计过程,在未来的某个时刻,易于修改实现。如果用户只接触ADT接口,应该可以在不“干扰”用户代码的情况下修改接口的实现。

A: 当然,一旦设计好ADT,必须仔细选择内部的数据结构,以使规定的操作的效率尽可能高。例如随机存取元素a,那么用链表表示就不够好,因为对链表来说,随机访问不是一个高效的操作,选择数据会得到更好的效果。

Q: 有序链表?

A: 在有序链表中,数据是按照关键值有序排列的,有序链表的删除常常是只限于删除在表头的最小(或最大)的节点。

A: 一般,在大多数需要使用有序数组的场合也可以使用有序链表。有序链表的优势在于插入的速度,因为元素不需要移动,而且链表可以随时扩展所需内存,数组只能局限于一个固定大小的内存。

A: 示例:SortedLinkedList.java

A: 当算法找到要插入的位置,用通常的方式插入数据项:把新节点的next字段指向下一个节点,然后把前一个结点的next字段指向新节点。然而,需要考虑一些特殊情况:节点有可能插在表头,或者表尾。

Q: 有序链表的效率?

A: 在有序链表插入或删除某一项最多需要O(N)次比较(平均N/2),因为必须沿着链表一步一步走才能找到正确的位置。然而,可以在O(1)的时间内找到或删除最小值,因为它总在表头。

A: 如果一个应用频繁地存取最小项,且不需要快速地插入,那么有序链表是一个有效的方案选择,例如,优先级队列可以用有序链表来实现。

Q: 链表插入排序(List Insertion Sort)?

A: 有序链表可以用于一种高效的排序机制。假设有一个无序数组,如果从这个数组中取出数据,然后一个一个地插入有序链表,它们自动地按照顺序排列。然后把它们从有序链表删除,重新放入数组,那么数组就排好序了。

A: 本质上与基于数组的插入排序是一样的,都是O(N2)的比较次数,只是说对于数组会有一半已存在的数据会涉及移动,相当于N2/4次移动,相比之下,链表只需2 * N次移动:一次是从数组到链表,一次是从链表到数组。

A: 不过链表插入有一个缺点:就是它要开辟差不多两倍的空间。

A: 示例: LinkedListSort.java

Q: 双向链表?

A: 双向链表提供了这样的能力,即允许向前遍历,也允许向后遍历整个链表,其中秘密在于它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。

A: 双向链表不必是双端链表(保持一个对链表最后一个元素的引用),但这种方式是很有用的。所以下面的示例将包含双端的性质。

Q: 基于双向链表的双端链表的Java实现?

A: 示例:DoublyLinkedList.java

A: addFirst(E)方法:将指定元素插入此列表的开头。 

A: addLast(E)方法:将指定元素添加到此列表的结尾。 

A: add(index, E)方法: 在此列表中指定的位置插入指定的元素。 

A: remove(Object o)方法: 从此列表中移除首次出现的指定元素(如果存在)。 

Q: 基于双向链表的双端队列?

A: 双向链表可以用来作为双端队列的基础。在双端队列中,可以从任何一头插入和删除,双向链表提供了这个能力。

Q: 为什么要引入迭代器的概念?

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: 迭代器模式就是提供一种方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节。

Q: 迭代器定义的接口?

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: 如下图显示了指向链表的某个结点的两个迭代器:

Q: JDK1.6的LinkedList的迭代器?

A: 迭代器类ListItr实现ListIterator接口,定义如下:

private class ListItr implements ListIterator<E> {

}

A: 示例:ListIteratorTestCase.java

Q: 迭代器指向哪里?

A: 迭代器类的一个设计问题是决定在不同的操作后,迭代器应该指向哪里。而JDK1.6中LinkedList.ListItr中的add()实现,next指针一直指向表头,这里假设调用的是iterator(),不指定下标。

Q: 本篇小结

  • 链表包含一个LinkedList对象和许多Entry对象。
  • next字段为null意味着链表的结尾。
  • 在表头插入结点需要把新结点的next字段指向原来的第一个结点,然后把header指向新结点。
  • 在表头删除结点要把header指向header.next。
  • 为了遍历链表,从header开始,然后从一个结点到下一个结点,方法是用每个结点的next字段找到下一个结点。
  • 通过遍历链表可以找到拥有特定值的结点,一旦找到,可以显示、删除或用其他方式操纵该结点。
  • 新结点可以插在某个特定值的结点的前面或后面,首先要遍历找到这个结点。
  • 双端链表在链表中维护一个指向最后一个结点的引用,它通常和header一样,叫做tailer。
  • 双端链表允许在表尾插入数据项。
  • 抽象数据类型是一种数据存储类,不涉及它的实现。
  • 栈和队列是ADT,它们既可以用数组实现,也可以用链表实现。
  • 有序链表中,结点按照关键字升序或降序排列。
  • 在有序链表中插入需要O(N) 的时间,因为必须要找到正确的插入点,最小值结点的删除需要O(1)时间。
  • 双向链表中,每个结点包含对前一个结点的引用,同时有对后一个结点的引用。
  • 双向链表允许反向遍历,并可以从表尾删除。
  • 迭代器是一个引用,它被封装在类对象中,这个引用指向相关联的链表中的结点。
  • 迭代器方法允许使用者沿链表移动迭代器,并访问当前所指的结点。
  • 能用迭代器遍历链表,在选定的结点上执行某些操作。

Java数据结构和算法 - 链表的更多相关文章

  1. java 数据结构与算法---链表

    原理来自百度百科  一.链表的定义 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运 ...

  2. Java数据结构和算法(一)线性结构之单链表

    Java数据结构和算法(一)线性结构之单链表 prev current next -------------- -------------- -------------- | value | next ...

  3. Java数据结构和算法(四)--链表

    日常开发中,数组和集合使用的很多,而数组的无序插入和删除效率都是偏低的,这点在学习ArrayList源码的时候就知道了,因为需要把要 插入索引后面的所以元素全部后移一位. 而本文会详细讲解链表,可以解 ...

  4. 【Java数据结构学习笔记之二】Java数据结构与算法之栈(Stack)实现

      本篇是java数据结构与算法的第2篇,从本篇开始我们将来了解栈的设计与实现,以下是本篇的相关知识点: 栈的抽象数据类型 顺序栈的设计与实现 链式栈的设计与实现 栈的应用 栈的抽象数据类型   栈是 ...

  5. java数据结构与算法之栈(Stack)设计与实现

    本篇是java数据结构与算法的第4篇,从本篇开始我们将来了解栈的设计与实现,以下是本篇的相关知识点: 栈的抽象数据类型 顺序栈的设计与实现 链式栈的设计与实现 栈的应用 栈的抽象数据类型 栈是一种用于 ...

  6. Java数据结构和算法 - 二叉树

    前言 数据结构可划分为线性结构.树型结构和图型结构三大类.前面几篇讨论了数组.栈和队列.链表都是线性结构.树型结构中每个结点只允许有一个直接前驱结点,但允许有一个以上直接后驱结点.树型结构有树和二叉树 ...

  7. Java数据结构和算法 - 高级排序

    希尔排序 Q: 什么是希尔排序? A: 希尔排序因计算机科学家Donald L.Shell而得名,他在1959年发现了希尔排序算法. A: 希尔排序基于插入排序,但是增加了一个新的特性,大大地提高了插 ...

  8. Java数据结构和算法 - 栈和队列

    Q: 栈.队列与数组的区别? A: 本篇主要涉及三种数据存储类型:栈.队列和优先级队列,它与数组主要有如下三个区别: A: (一)程序员工具 数组和其他的结构(栈.队列.链表.树等等)都适用于数据库应 ...

  9. Java数据结构和算法 - OverView

    Q: 为什么要学习数据结构与算法? A: 如果说Java语言是自动档轿车,C语言就是手动档吉普.数据结构呢?是变速箱的工作原理.你完全可以不知道变速箱怎样工作,就把自动档的车子从1档开到4档,而且未必 ...

随机推荐

  1. 多进程Multiprocessing模块

    多进程 Multiprocessing 模块 先看看下面的几个方法: star() 方法启动进程, join() 方法实现进程间的同步,等待所有进程退出. close() 用来阻止多余的进程涌入进程池 ...

  2. centos网络配置方法(手动设置,自动获取)

    不知道为什么最近一段时间网络特别的慢,还老是断,断的时候,局域网都连不上,当我手动设置一下ip后就可以了,搞得我很无语.下面是2种设置网络连接的方法,在说怎么设置前,一定要做好备份工作,特别是对于新手 ...

  3. poj-3522 最小生成树

    Description Given an undirected weighted graph G, you should find one of spanning trees specified as ...

  4. BZOJ_2152_聪聪可可_点分治

    BZOJ_2152_聪聪可可_点分治 Description 聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃.两个人都想玩儿电脑(可是他们家只有一台电脑)…… ...

  5. laravel rbac的用户 角色 权限的crud

    user.php <?php /* |-------------------------------------------------------------------------- | W ...

  6. 聊聊Socket、TCP/IP、HTTP、FTP及网络编程

    1 这些都是什么 既然是网络传输,涉及几个系统之间的交互,那么首先要考虑的是如何准确的定位到网络上的一台或几台主机,另一个是如何进行可靠高效的数据传输.这里就要使用到TCP/IP协议. 1.1 TCP ...

  7. 树莓派使用modbus与stm32通信

    树莓派+stm32开发板通信树莓派上使用java+jamod实现.jamod官网stm32使用freemodbus实现 ​

  8. 深入理解java虚拟机之java内存区域

    java虚拟机在执行java程序的时候会把它所管理的内存分为多个不同的区域,每个区域都有不同的作用,以及由各自的生命周期,有些随着虚拟机进行的启动而存在,有些区域则依赖于用户线程的启动或结束而建立或销 ...

  9. TensorFlow从1到2(三)数据预处理和卷积神经网络

    数据集及预处理 从这个例子开始,相当比例的代码都来自于官方新版文档的示例.开始的几个还好,但随后的程序都将需要大量的算力支持.Google Colab是一个非常棒的云端实验室,提供含有TPU/GPU支 ...

  10. XiaomiPushDemo【小米推送集成,基于V3.6.12版本】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 这个Demo只是记录小米推送的集成,不能运行. 使用步骤 一.项目组织结构图 注意事项: 1.  导入类文件后需要change包名以 ...