一、LinkedList概述

  LinkedList的底层数据结构为双向链表结构,与ArrayList相同的是LinkedList也可以存储相同或null的元素。相对于ArrayList来说,LinkedList的插入与删除的速度更快,时间复杂度为O(1),查找的速度就相对比较慢了,因为每次遍历的时候都必须从链表的头部或者链表的尾部开始遍历,时间复杂度为O(n/2)。为了实现快速插入或删除数据,LinkedList在每个节点都维护了一个前继节点和一个后续节点,这是一种典型的以时间换空间的思想。LinkedList同时也可以实现栈与队列的功能。

  二、LinkedList的结构图

  在LinkedList中每个节点有会有两个指针,一个指向前一个节点,另一个指向下一个节点。链表的头部的前指针为null,尾部的后指针也为null,因此也可以说明LinkedList(基于jdk1.8)是非循环双向链表结构。源码如下:

  1. private static class Node<E> {
  2. E item;
  3. Node<E> next;
  4. Node<E> prev;
  5.  
  6. Node(Node<E> prev, E element, Node<E> next) {
  7. this.item = element;
  8. this.next = next;
  9. this.prev = prev;
  10. }
  11. }

  这是一个私有静态内部类

  三、LinkedList属性

  1、size: 链表的长度

  2、first:链表的第一个节点

  3、last:链表的最后一个节点

  1. transient int size = ;
  2.  
  3. /**
  4. * Pointer to first node.
  5. * Invariant: (first == null && last == null) ||
  6. * (first.prev == null && first.item != null)
  7. */
  8. transient Node<E> first;
  9.  
  10. /**
  11. * Pointer to last node.
  12. * Invariant: (first == null && last == null) ||
  13. * (last.next == null && last.item != null)
  14. */
  15. transient Node<E> last;
  16.  
  17. /**
  18. * Constructs an empty list.
  19. */

  四、添加节点

  1、链表头部添加新节点

  1. /**
  2. * Links e as first element.
  3. * 链接头部
  4. */
  5. private void linkFirst(E e) {
  6. //链表的第一个节点
  7. final Node<E> f = first;
  8. //创建节点
  9. final Node<E> newNode = new Node<>(null, e, f);
  10. //将新创建的节点放到链条头部
  11. first = newNode;
  12. //当链表为null时,链表头部和尾部都指向新节点
  13. if (f == null)
  14. last = newNode;
  15. else
  16. f.prev = newNode;//把原本第一个节点的前一个节点指向新的节点
  17. size++;//链表长度加1
  18. modCount++;//链表修改次数加1
  19. }

  当链表为空的时候比较简单,直接将链表的头部和尾部都指向新节点即可,下面我来说一下在非空的情况下头部插入新节点:

  2、往链表尾部插入新节点

  1. /**
  2. * Links e as last element.
  3. */
  4. void linkLast(E e) {
  5. //原来的最后一个节点
  6. final Node<E> l = last;
  7. //创建新的节点,next为null
  8. final Node<E> newNode = new Node<>(l, e, null);
  9. //将新节点指向最后一个节点
  10. last = newNode;
  11. if (l == null)
  12. first = newNode;//链表为空时第一个节点也指向新节点
  13. else
  14. l.next = newNode;//将原最后一个节点的next指针指向新节点
  15. size++;
  16. modCount++;
  17. }

  具体流程:

  3、在指定节点之前插入新节点

  1. /**
  2. * Inserts element e before non-null Node succ.
  3. * 指定节点之前插入新节点
  4. */
  5. void linkBefore(E e, Node<E> succ) {
  6. // assert succ != null;
  7. //指定的节点的前一个节点
  8. final Node<E> pred = succ.prev;
  9. //待插入的新节点,新节点的前一个节点为 指定节点的前一个节点,下一个节点为指定节点
  10. final Node<E> newNode = new Node<>(pred, e, succ);
  11. //指定节点的前一个节点指向新节点
  12. succ.prev = newNode;
  13. if (pred == null)
  14. first = newNode;//如果指定节点为第一个节点,那么将节点设置为头部
  15. else
  16. pred.next = newNode;//否则将前一个的下一个节点指向新节点
  17. size++;
  18. modCount++;
  19. }

流程:

  

  五、删除节点

  1、删除第一个节点

  1. /**
  2. * Unlinks non-null first node f.
  3. * 删除第一个节点
  4. */
  5. private E unlinkFirst(Node<E> f) {
  6. // assert f == first && f != null;
  7. //第一个节点
  8. final E element = f.item;
  9. //第一个节点的前一个节点
  10. final Node<E> next = f.next;
  11. //将前一个节点和原第一个节点掷为空,方便回收
  12. f.item = null;
  13. f.next = null; // help GC
  14. //把原第一个节点设置成第一个节点
  15. first = next;
  16. //链表只有一个节点的情况
  17. if (next == null)
  18. last = null;
  19. else
  20. next.prev = null;//将原节点的下一个的前一个节点设置为null,因为该节点已经设置为第一个节点,而第一个节点的前一个节点为null
  21. size--;
  22. modCount++;
  23. return element;
  24. }

  流程:

  2、删除链表最后一个节点

  1. /**
  2. * Unlinks non-null last node l.
  3. * 删除最后一个节点
  4. */
  5. private E unlinkLast(Node<E> l) {
  6. // assert l == last && l != null;
  7. //最后一个节点
  8. final E element = l.item;
  9. //最后一个节点的前一个节点
  10. final Node<E> prev = l.prev;
  11. l.item = null;
  12. l.prev = null; // help GC
  13. last = prev;
  14. //只有一个节点的情况
  15. if (prev == null)
  16. first = null;
  17. else
  18. prev.next = null;//将前一个节点的下一个节点掷为null
  19. size--;
  20. modCount++;
  21. return element;
  22. }

  流程:

  3、删除指定节点

  1. /**
  2. * Unlinks non-null node x.
  3. * 删除指定节点
  4. */
  5. E unlink(Node<E> x) {
  6. // assert x != null;
  7. //指定节点的数据
  8. final E element = x.item;
  9. //指定节点的下一个节点
  10. final Node<E> next = x.next;
  11. //指定节点的前一个节点
  12. final Node<E> prev = x.prev;
  13.  
  14. //指定节点为第一个节点,将下一个节点设置为第一个节点
  15. if (prev == null) {
  16. first = next;
  17. } else {//否则,将指定节点的前一个节点指向指定节点的下一个节点
  18. prev.next = next;
  19. x.prev = null;
  20. }
  21. //指定节点为最后一个节点,将前一个节点设置为最后一个节点
  22. if (next == null) {
  23. last = prev;
  24. } else {//否则,
  25. next.prev = prev;
  26. x.next = null;
  27. }
  28.  
  29. x.item = null;
  30. size--;
  31. modCount++;
  32. return element;
  33. }

  流程:

  六、添加数据

  1、add方法:

  1. /**
  2. * Appends the specified element to the end of this list.
  3. *
  4. * <p>This method is equivalent to {@link #addLast}.
  5. *
  6. * @param e element to be appended to this list
  7. * @return {@code true} (as specified by {@link Collection#add})
  8. */
  9. public boolean add(E e) {
  10. //向链表的最后位置插入一个节点
  11. linkLast(e);
  12. return true;
  13. }

  2、addFirst方法:

  1. /**
  2. * Inserts the specified element at the beginning of this list.
  3. *
  4. * @param e the element to add
  5. */
  6. public void addFirst(E e) {
  7. linkFirst(e);
  8. }

  具体的插入流程可参照第4部分;

  3、addLast方法:

  1. /**
  2. * Appends the specified element to the end of this list.
  3. *
  4. * <p>This method is equivalent to {@link #add}.
  5. *
  6. * @param e the element to add
  7. */
  8. public void addLast(E e) {
  9. linkLast(e);
  10. }

  具体流程参照第四部分的linkLast方法解释;

  七、获取数据

  获取数据也是分为3个方法,获取链表头部的节点数据,尾部节点数据和其他的节点数据。获取头部和尾部比简单,直接获取first节点或last节点就可以了,这里我们主要看一下是怎么获取其他的节点:

  1. /**
  2. * Returns the element at the specified position in this list.
  3. *
  4. * @param index index of the element to return
  5. * @return the element at the specified position in this list
  6. * @throws IndexOutOfBoundsException {@inheritDoc}
  7. */
  8. public E get(int index) {
  9. checkElementIndex(index);
  10. return node(index).item;
  11. }

  从源码中可以看到,获取其他节点的数据时,是根据下标来获取的,首先先检查输入的index下标是否有越界的嫌疑,然后node方法,下面我们看一下node方法具体实现方式:

  1. /**
  2. * Returns the (non-null) Node at the specified element index.
  3. */
  4. Node<E> node(int index) {
  5. // assert isElementIndex(index);
  6. /**
  7. * 传入的index如果大于链表长度的一半,那个从链表后面向前遍历
  8. * 否则,从前面开始遍历
  9. */
  10. if (index < (size >> )) {
  11. Node<E> x = first;
  12. for (int i = ; i < index; i++)
  13. x = x.next;
  14. return x;
  15. } else {
  16. Node<E> x = last;
  17. for (int i = size - ; i > index; i--)
  18. x = x.prev;
  19. return x;
  20. }
  21. }

  从代码中可以看到,如果使用get(index)方法时,每一次都需要从头部或尾部开始遍历,效率比较低。如果要遍历LinkedList,也不推荐这种方式。

  八、删除数据

  删除数据也是3中方法,只讲删除其他节点数据的方法:

  1. /**
  2. * Removes the first occurrence of the specified element from this list,
  3. * if it is present. If this list does not contain the element, it is
  4. * unchanged. More formally, removes the element with the lowest index
  5. * {@code i} such that
  6. * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
  7. * (if such an element exists). Returns {@code true} if this list
  8. * contained the specified element (or equivalently, if this list
  9. * changed as a result of the call).
  10. *
  11. * @param o element to be removed from this list, if present
  12. * @return {@code true} if this list contained the specified element
  13. */
  14. public boolean remove(Object o) {
  15. if (o == null) {//为null的情况,从头部开始查找
  16. for (Node<E> x = first; x != null; x = x.next) {
  17. if (x.item == null) {
  18. unlink(x);
  19. return true;
  20. }
  21. }
  22. } else {//非null,从头部开始查找,然后删除掉
  23. for (Node<E> x = first; x != null; x = x.next) {
  24. if (o.equals(x.item)) {
  25. unlink(x);
  26. return true;
  27. }
  28. }
  29. }
  30. return false;
  31. }

  从源码中可以看到,在删除元素的时候是从第一个节点开始一个一个遍历,通过equals方法的来获取到需要删除节点,然后调用unlinke方法将节点删除掉的。

  九、实现stack相关方法

  栈的数据结构实现了FIFO的顺序,即先进先出的规则。

  1、push方法:

  1. /**
  2. * Pushes an element onto the stack represented by this list. In other
  3. * words, inserts the element at the front of this list.
  4. *
  5. * <p>This method is equivalent to {@link #addFirst}.
  6. *
  7. * @param e the element to push
  8. * @since 1.6
  9. */
  10. public void push(E e) {
  11. addFirst(e);
  12. }

  每次添加数据的时候都是添加到链表头部。

  2、pop方法:

  1. /**
  2. * Pops an element from the stack represented by this list. In other
  3. * words, removes and returns the first element of this list.
  4. *
  5. * <p>This method is equivalent to {@link #removeFirst()}.
  6. *
  7. * @return the element at the front of this list (which is the top
  8. * of the stack represented by this list)
  9. * @throws NoSuchElementException if this list is empty
  10. * @since 1.6
  11. */
  12. public E pop() {
  13. return removeFirst();
  14. }

  往栈中获取一个数据,同时也将栈的第一个数据删除。

  3、peek方法:

  1. /**
  2. * Retrieves, but does not remove, the head (first element) of this list.
  3. *
  4. * @return the head of this list, or {@code null} if this list is empty
  5. * @since 1.5
  6. */
  7. public E peek() {
  8. final Node<E> f = first;
  9. return (f == null) ? null : f.item;
  10. }

  查看栈中的第一个数据,跟pop方法的区别是peek方法只是查看数据,并没有删除数据,pop是从栈中弹出一个数据,需要从栈中删除数据。

  十、实现queue方法

  队列也是我们在开发的过程经常使用到数据结构,比如消息队列等,队列的特点是每次添加数据的时候都是添加大队列的尾部,获取数据时总是从头部拉取。基于以上特点,我们可以使用LinkedList中的linkLast方式实现数据的添加,使用unLinkfirst方法实现数据的拉取,使用getFisrt方法实现数据的查看,源码如下:

  1、添加数据:

  1. /**
  2. * Adds the specified element as the tail (last element) of this list.
  3. *
  4. * @param e the element to add
  5. * @return {@code true} (as specified by {@link Queue#offer})
  6. * @since 1.5
  7. */
  8. public boolean offer(E e) {
  9. return add(e);
  10. }

  2、拉取数据:

  1. /**
  2. * Retrieves and removes the head (first element) of this list.
  3. *
  4. * @return the head of this list, or {@code null} if this list is empty
  5. * @since 1.5
  6. */
  7. public E poll() {
  8. final Node<E> f = first;
  9. return (f == null) ? null : unlinkFirst(f);
  10. }

  3、查看数据:

  1. /**
  2. * Retrieves, but does not remove, the head (first element) of this list.
  3. *
  4. * @return the head of this list, or {@code null} if this list is empty
  5. * @since 1.5
  6. */
  7. public E peek() {
  8. final Node<E> f = first;
  9. return (f == null) ? null : f.item;
  10. }

  

  十一、LinkedList使用注意事项

  1、LinkedList是非线程安全的,在多线程的环境下可能会发生不可预知的结果,所以在多线程环境中谨慎使用它,可以转换成线程类,或是使用线程安全的集合类来代替LinkedList的使用。

  2、遍历LinkedList中的数据的时候,切记别使用fori方式(即随机顺序访问get(index))去遍历,建议使用迭代器或foreach方式遍历。原因在上面的源码中也说到过,可以看一下第七部分数据获取中,使用get(index)方法获取数据时每次都是链表头部或尾部开始遍历,这样是非常不合理的,时间复杂度为O(n^2)。在数据量较小的情况下是没有什么区别,但是数据上去之后,可能会出现程序假死的现象。测试如下:

  1. public static void main(String[] args) throws Exception {
  2.  
  3. List<Integer> list = new LinkedList<>();
  4. for (int i = ; i < ; i++) {
  5. list.add(i);
  6. }
  7.  
  8. long start = System.currentTimeMillis();
  9. for (int i = ; i < list.size(); i++) {
  10. list.get(i);
  11. }
  12. long end = System.currentTimeMillis();
  13. System.out.println("使用fori方式所需时间:" + (end - start));
  14.  
  15. start = System.currentTimeMillis();
  16. for (Integer integer : list) {
  17.  
  18. }
  19. end = System.currentTimeMillis();
  20. System.out.println("使用foreach方式所需时间:" + (end - start));
  21. start = System.currentTimeMillis();
  22. Iterator<Integer> iterator = list.iterator();
  23. while (iterator.hasNext()){
  24. Integer next = iterator.next();
  25. }
  26. end = System.currentTimeMillis();
  27. System.out.println("使用迭代器方式所需时间:" + (end - start));
  28. }

  三种遍历10万条数据所需要时间:

  1. 使用fori方式所需时间:
  2. 使用foreach方式所需时间:
  3. 使用迭代器方式所需时间:

  从结果中可以看到,使用迭代器或foreach方式比fori方式快的不是十倍百倍,原因是使用foreach和迭代器的时候每次获取数据后都记录当前的位置index,当下个循环的时候直接在index+1处获取即可,而不需要从新在头部或尾部开始遍历了。

  十二、总结

  1、LinkedList是非线程安全的。

  2、LinkedList可以存储null值或重复的数据。

  3、LinkedList底层存储结构为双向链式非循环结构,这种结构添加删除的效率高于查询效率。

  4、与ArrayList相比较,LinkedList的删除添加数据效率要比ArrayList高,查询数据效率低于ArrayList。

  5、LinkedList可以用于实现stack和queue数据结构,比如:Queue<T> queue = new LinkedList<T>();

  6、遍历数据时切勿使用随机访问方式遍历,推荐使用foreach或迭代器遍历。

  7、如果文章中有什么写得不对的地方,欢迎大家指出来。

jdk源码阅读笔记-LinkedList的更多相关文章

  1. jdk源码阅读笔记-LinkedHashMap

    Map是Java collection framework 中重要的组成部分,特别是HashMap是在我们在日常的开发的过程中使用的最多的一个集合.但是遗憾的是,存放在HashMap中元素都是无序的, ...

  2. jdk源码阅读笔记-HashSet

    通过阅读源码发现,HashSet底层的实现源码其实就是调用HashMap的方法实现的,所以如果你阅读过HashMap或对HashMap比较熟悉的话,那么阅读HashSet就很轻松,也很容易理解了.我之 ...

  3. jdk源码阅读笔记-ArrayList

    一.ArrayList概述 首先我们来说一下ArrayList是什么?它解决了什么问题?ArrayList其实是一个数组,但是有区别于一般的数组,它是一个可以动态改变大小的动态数组.ArrayList ...

  4. jdk源码阅读笔记

    1.环境搭建 http://www.komorebishao.com/2020/idea-java-jdk-funyard/ 2. 好的源码阅读资源 https://zhuanlan.zhihu.co ...

  5. jdk源码阅读笔记-Integer

    public final class Integer extends Number implements Comparable<Integer> Integer 由final修饰了,所以该 ...

  6. JDK源码学习笔记——LinkedList

    一.类定义 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E& ...

  7. jdk源码阅读笔记-HashMap

    文章出处:[noblogs-it技术博客网站]的博客:jdk1.8源码分析 在Java语言中使用的最多的数据结构大概右两种,第一种是数组,比如Array,ArrayList,第二种链表,比如Array ...

  8. jdk源码阅读笔记-AbstractStringBuilder

    AbstractStringBuilder 在java.lang 包中,是一个抽象类,实现 Appendable 接口和 CharSequence 接口,这个类的诞生是为了解决 String 类在创建 ...

  9. jdk源码阅读笔记-String

    本人自学java两年,有幸初入这个行业,所以功力尚浅,本着学习与交流的态度写一些学习随笔,什么错误的地方,热烈地希望园友们提出来,我们共同进步!这是我入园写的第一篇文章,写得可能会很乱. 一.什么是S ...

随机推荐

  1. 用post请求方式实现对地图服务的基本操作

    ArcGIS Server REST API 中的很多操作都可以用以下方式实现,具体参数的设置请查看其中的详细说明 public List<string> getGeometry(stri ...

  2. 安装ie时,报:此安装不支持您的操作系统的当前语言

    打开注册表(win的"运行"栏键入 regedit 再按 OK )的HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Nls/ ...

  3. Flask入门之Jinjia模板的一些语法

    1. 变量表示 {{ argv }} 2. 赋值操作 {% set links = [ ('home',url_for('.home')), ('service',url_for('.service' ...

  4. jieba库分词

    (1)团队简介的词频统计 import jieba import collections s="制作一个购票小程序,这个购票小程序可以根据客户曾经的购票历史" s+="和 ...

  5. 对于Javascript 执行上下文的理解

    转载无源头地址 在这篇文章中,将比较深入地阐述下执行上下文 – JavaScript中最基础也是最重要的一个概念.相信读完这篇文章后,你就会明白javascript引擎内部在执行代码以前到底做了些什么 ...

  6. Oracle-12:伪列rowid和rownum

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 伪列:不真实存储在真表中,但是我们可以查询到不能对伪列进行增删改操作! 分页可以用rownum来分!!!!!! ...

  7. 网络模型 - 每天5分钟玩转 Docker 容器技术(169)

    本节我们讨论 Kubernetes 网络这个重要主题. Kubernetes 作为编排引擎管理着分布在不同节点上的容器和 Pod.Pod.Service.外部组件之间需要一种可靠的方式找到彼此并进行通 ...

  8. Android/Linux Thermal Governor之IPA分析与使用

    IPA(Intelligent Power Allocator)模型的核心是利用PID控制器,Thermal Zone的温度作为输入,可分配功耗值作为输出,调节Allocator的频率和电压值. 由P ...

  9. An annotation based command line parser

    Java命令行选项解析之Commons-CLI & Args4J & JCommander http://rensanning.iteye.com/blog/2161201 JComm ...

  10. Java基础系列--冒泡排序

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9064218.html 1.算法简介 冒牌排序是很耳熟的排序方式,虽然它使用的很少,但是经 ...