链表(Linked list)是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的。链表由一系列结点(链表中的每一个元素称为结点)组成,结点能够在运行时动态生成。每一个结点包括两部分:一个是存储数据元素的数据域,另外一个是存储下一个结点地址的指针域。java
相比于数组,链表是一种稍微复杂一点的数据结构。对于这两个很是基础、很是经常使用的数据结构,咱们经常会放到一起来比较。因此咱们先来看,这二者有什么区别。node
首先咱们从底层存储结构来看:web
数组:须要一块连续的存储空间,对内存的要求比较高,好比咱们要申请一个1000M的数组,若是内存中没有连续的足够大的存储空间则会申请失败,即使内存的剩余可用空间大于1000M,仍然会申请失败。数组
链表:与数组相反,它并不须要一块连续的内存空间,它经过指针将一组零散的内存块串联起来使用,若是咱们须要申请一个1000M大小的链表,只要内存剩余的可用空间大于1000M,便不会出现问题。数据结构
下图对比了链表和数组的存储结构:svg
此时咱们在来回顾咱们刚刚提到的链表的概念:物理存储单元上非连续、非顺序,元素的逻辑顺序是经过链表中的指针连接次序实现的,链表由一系列结点组成,每一个结点包括两个部分:一个是存储数据元素数据域,另外一个是存储下一个结点地址的指针域函数
固然了这只是咱们提到的链表的最基本的存储形式,其实链表有不少种不一样的类型,分别对应了不一样的结构,接下来咱们就来分析不一样的链表类型源码分析
所谓的单链表就是咱们刚刚讲到的链表的最基本的结构,链表经过指针将一组零散的内存块串联在一块儿。其中,咱们把内存块称为链表的“结点”。为了将全部的结点串起来,每一个链表的结点除了存储数据以外,还须要记录链上的下一个结点的地址。咱们把这个记录下个结点地址的指针叫做后继指针next,若是链表中的某个结点为p,p的下一个结点为q,咱们能够表示为:p->next=qpost
下面的图更加详细的描述了单链表的存储结构性能
从图中,应该能够发现,其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。咱们习惯性地把第一个结点叫做头结点,把最后一个结点叫做尾结点。其中,头结点用来记录链表的基地址,有了它,咱们就能够遍历获得整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。
与数组同样,链表也支持数据的查找、插入和删除操做。
咱们知道,在进行数组的插入、删除操做时,为了保持内存数据的连续性,须要作大量的数据搬移。而在链表中插入或者删除一个数据,咱们并不须要为了保持内存的连续性而搬移节点,由于链表的存储空间自己就不是连续的。因此,在链表中插入和删除一个数据是很是快速的。
为了方便理解,我画了一张图,从图中咱们能够看出,针对链表的插入和删除操做,咱们只须要考虑相邻结点的指针改变便可。
可是,有利就有弊。链表想要随机访问第k个元素,就没有数组那么高效了。由于链表中的数据并不是连续存储的,因此没法像数组那样,根据首地址和下标,经过寻址公式就能直接计算出对应的内存地址,而是须要根据指针一个结点一个结点的一次遍历,直到找到相应的结点,因此,链表随机访问的性能没有数组好。
循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表惟一的区别就在尾结点。咱们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从下图中,应该能够看出来,它像一个环同样首尾相连,因此叫做“循环”链表,和单链表相比,循环链表的优势是从链尾到链头比较方便。当要处理的数据具备环型结构特色时,就特别适合采用循环链表,循环链表的结构如图所示
单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每一个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点,如图所示
从图中能够看出来,双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。因此,若是存储一样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但能够支持双向遍历,这样也带来了双向链表操做的灵活性。
了解了循环链表和双向链表,若是把这两种链表整合在一块儿就是一个双向循环链表 。
数组和链表是两种大相径庭的内存组织方式。正是由于内存存储的区别,它们插入、删除、随机访问操做的性能正好相反。
数组简单易用,在实现上使用的是连续的内存空间,缺点是大小固定,一经声明就要占用整块连续内存空间。若是声明的数组过大,系统可能没有足够的连续内存空 间分配给它,致使“内存不足(out of memory)”。若是声明的数组太小,则可能出现不够用的状况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,很是费时。
链表自己没有大小的限制,自然地支持动态扩容,我以为这也是它与数组最大的区别。你可能会说,咱们 Java 中的 ArrayList 容器,也能够支持动态扩容啊?咱们以前已经说过,当咱们往支持动态扩容的数组中插入一个数据时,若是数组中没有空闲空间了,就会申请一个更大的空间,将数据拷贝过去,而数据拷贝的操做是很是耗时的。
因此:数组的优点是查询速度很是快,可是增删改慢;链表的优点是增删改快,可是查询慢。
学习数组的时候咱们分析了基于数组而实现的 ArrayList 集合,那 java 中也有基于链表实现的集合 LinkedList,接下来咱们就来分析一下 LinkedList 的底层源码
首先咱们写一段最简单的代码以下 :
public static void main(String[] args) { // 构建容器 List<String> list = new LinkedList<String>(); // 向容器中添加数据 list.add("test01"); list.add("test02"); }
而后咱们以断点调试的方式进入底层源码进行查看,咱们主要关注容器的构建和元素的添加
点开 LinkedList 源码查看 LinkedList 类的声明及属性
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{ /** 链表的长度*/ transient int size = 0; /** 存储整个链表的头结点*/ transient Node<E> first; /** 存储整个链表的尾结点*/ transient Node<E> last; /** *LinkedList的构造函数 */ public LinkedList() { } /** *静态内部类Node,也就是咱们链表中的概念;结点 */ private static class Node<E> { /** 结点中存储的数据*/ E item; /** 结点中存储的后继指针next*/ Node<E> next; /** 结点中存储的前驱指针prev*/ Node<E> prev; /** 结点的构造函数*/ Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } /** *添加数据方法 */ public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } }
调试过程如图所示:
经过分析咱们能够知道,LinkedList 内部采用双向链表实现数据的存储,那接下来咱们来分析一下,如何从 LinkedList 容器中获取数据
从 LinkedList 中获取元素的代码以下:
public static void main(String[] args) { // 构建容器 List<String> list = new LinkedList<String>(); // 向容器中添加数据 list.add("test01"); list.add("test02"); // 获取元素 String s = list.get(1); System.out.println(s); }
查看 LinkedList 中的 get 方法,源码以下:
/** *经过索引获取数据 *@params index 索引 */ public E get(int index) { //判断索引是否越界 checkElementIndex(index); return node(index).item; } private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private boolean isElementIndex(int index) { //若是索引不在 [0 , size), 范围内就抛出索引越界异常,其中 size是集合的长度 return index >= 0 && index < size; } // 根据索引从链表中获取节点 Node<E> node(int index) { //若是索引小于集合长度的一半则从头节点开始遍历,不然从尾节点开始遍历,由于底层用的是双向链表支持 // 先后查询遍历 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
代码跟踪效果以下:
至此,关于 LinkedList 的源码解析就告一段落了,我只分析了构建集合,向集合中添加元素以及根据索引从集合中获取元素的相关方法,其余方法的分析你们能够尝试着本身分析一下。
前面咱们分析过 ArrayList 的源码,如今又分析了 LinkedList 源码,接下来将两者进行比较:
1:ArrayList 的实现基于数组,LinkedList 的实现基于双向链表。
2:对于随机访问,ArrayList 优于LinkedList,ArrayList 能够根据下标对元素进行随机访问。而 LinkedList 的每个元素都依靠地址指针和它后一个元素链接在一块儿,在这种状况下,查找某个元素只能从链表头开始查询直到找到为止。
3:对于插入和删除操做,LinkedList 优于 ArrayList,由于当元素被添加到LinkedList任意位置的时候,不须要像 ArrayList 那样从新计算大小或者是更新索引。
4:LinkedList 比 ArrayList 更占内存,由于LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
经过今天的学习能够了解到:
概念:链表(Linked list)是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的。
特色:
应用:
ArrayList和LinkedList的比较
至此,链表部分的学习就完成了。
本文掘金同步上传:数据结构-线性表-链表