数据结构-线性表-链表

链表

概念

链表(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 的底层源码

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 的源码解析就告一段落了,我只分析了构建集合,向集合中添加元素以及根据索引从集合中获取元素的相关方法,其余方法的分析你们能够尝试着本身分析一下。

LinkedList和ArrayList的比较

前面咱们分析过 ArrayList 的源码,如今又分析了 LinkedList 源码,接下来将两者进行比较:

1:ArrayList 的实现基于数组,LinkedList 的实现基于双向链表。

2:对于随机访问,ArrayList 优于LinkedList,ArrayList 能够根据下标对元素进行随机访问。而 LinkedList 的每个元素都依靠地址指针和它后一个元素链接在一块儿,在这种状况下,查找某个元素只能从链表头开始查询直到找到为止。

3:对于插入和删除操做,LinkedList 优于 ArrayList,由于当元素被添加到LinkedList任意位置的时候,不须要像 ArrayList 那样从新计算大小或者是更新索引。

4:LinkedList 比 ArrayList 更占内存,由于LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

总结

经过今天的学习能够了解到:

概念:链表(Linked list)是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的。

特色

  • 高效的插入和删除
  • 低效的随机访问

应用

  • LinkedList的源码分析(容器构建、添加元素及获取元素)

ArrayList和LinkedList的比较

至此,链表部分的学习就完成了。

本文掘金同步上传:数据结构-线性表-链表