一、LinkedList

1.定义

LinkedList:List接口链表的实现

  • LinkedList是双向链表
  • 基于链表实现的方式使得LinkedList在插入和删除时更优于ArrayList
  • LinkedList随机访问逊色于ArrayList
  • LinkedList实现所有可选的列表操作
  • 允许所有的元素包括null
  • LinkedList实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作
  • 与ArrayList一样此实现不是同步的
public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable

从LinkedList定义代码中可以看出:

  • LinkedList继承AbstractSequentialList
  • 实现List、Deque、Cloneable、Serializable接口
  • AbstractSequentialList提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度(???求解答)
  • Deque一个线性 collection,支持在两端插入和移除元素,定义了双端队列的操作

2.属性

在LinkedList中提供了两个基本属性:

  • size(private transient Entry header = new Entry(null, null, null)),size表示的LinkedList的大小
  • header(private transient Entry header = new Entry(null, null, null)),header表示链表的表头,Entry为节点对象

Entry对象源代码:

private static class Entry<E> {E element;        //元素节点Entry<E> next;    //下一个元素Entry<E> previous;  //上一个元素Entry(E element, Entry<E> next, Entry<E> previous) {this.element = element;this.next = next;this.previous = previous;}
}//Entry为LinkedList的内部类,它定义了存储的元素。
//还定义了该元素的前一个元素、后一个元素,这是典型的双向链表定义方式。

3.构造方法

LinkedList提供了两个构造方法:

  • LinkedList():构造一个空列表(里面没有任何元素,仅仅只是将header节点的前一个元素、后一个元素都指向自身)
  • LinkedList(Collection<? extends E> c):构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。该构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中
/***  构造一个空列表。*/public LinkedList() {header.next = header.previous = header;}/***  构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。*/public LinkedList(Collection<? extends E> c) {this();addAll(c);}/***  添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。*/public boolean addAll(Collection<? extends E> c) {return addAll(size, c);}/*** 将指定 collection 中的所有元素从指定位置开始插入此列表。其中index表示在其中插入指定collection中第一个元素的索引*/
public boolean addAll(int index, Collection<? extends E> c) {//若插入的位置小于0或者大于链表长度,//则抛出IndexOutOfBoundsException异常if (index < 0 || index > size)throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);Object[] a = c.toArray();int numNew = a.length;    //插入元素的个数//若插入的元素为空,则返回falseif (numNew == 0)return false;//modCount:在AbstractList中定义的,表示从结构上修改列表的次数modCount++;//获取插入位置的节点,若插入的位置在size处,则是头节点,否则获取index位置处的节点Entry<E> successor = (index == size ? header : entry(index));//插入位置的前一个节点,在插入过程中需要修改该节点的next引用:指向插入的节点元素Entry<E> predecessor = successor.previous;//执行插入动作for (int i = 0; i < numNew; i++) {//构造一个节点e,这里已经执行了插入节点动作同时修改了相邻节点的指向引用//Entry<E> e = new Entry<E>((E) a[i], successor, predecessor);//将插入位置前一个节点的下一个元素引用指向当前元素predecessor.next = e;//修改插入位置的前一个节点,//这样做的目的是将插入位置右移一位,//保证后续的元素是插在该元素的后面,确保这些元素的顺序predecessor = e;}successor.previous = predecessor;//修改容量大小size += numNew;return true;
}//在addAll()方法中,涉及到了两个方法,//一个是entry(int index),该方法为LinkedList的私有方法,//主要是用来查找index位置的节点元素。/*** 返回指定位置(若存在)的节点元素*/private Entry<E> entry(int index) {if (index < 0 || index >= size)throw new IndexOutOfBoundsException("Index: " + index + ", Size: "+ size);//头部节点Entry<E> e = header;//判断遍历的方向if (index < (size >> 1)) {for (int i = 0; i <= index; i++)e = e.next;} else {for (int i = size; i > index; i--)e = e.previous;}return e;}

4.Linkedlist增加方法,移除方法,查找方法

增加方法

//add(E e): 将指定元素添加到此列表的结尾。public boolean add(E e) {addBefore(e, header);return true;}//该方法调用addBefore方法,然后直接返回true,//对于addBefore()而已,它为LinkedList的私有方法。private Entry<E> addBefore(E e, Entry<E> entry) {//利用Entry构造函数构建一个新节点 newEntry,Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);//修改newEntry的前后节点的引用,确保其链表的引用关系是正确的newEntry.previous.next = newEntry;newEntry.next.previous = newEntry;//容量+1size++;//修改次数+1modCount++;return newEntry;}

其他增加方法

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

addAll(Collection<? extends E> c):添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。

addAll(int index, Collection<? extends E> c):将指定 collection 中的所有元素从指定位置开始插入此列表。

AddFirst(E e): 将指定元素插入此列表的开头。

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

移除方法

  • 首先判断移除的元素是否为null
  • 其次迭代这个链表找到该元素节点
  • 最后调用remove(Entry e)(remove(Entry e)为私有方法,是LinkedList中所有移除方法的基础方法)
//remove(Object o):
//从此列表中移除首次出现的指定元素(如果存在)public boolean remove(Object o) {if (o==null) {for (Entry<E> e = header.next; e != header; e = e.next) {if (e.element==null) {remove(e);return true;}}} else {for (Entry<E> e = header.next; e != header; e = e.next) {if (o.equals(e.element)) {remove(e);return true;}}}return false;}private E remove(Entry<E> e) {if (e == header)throw new NoSuchElementException();//保留被移除的元素:要返回E result = e.element;//将该节点的前一节点的next指向该节点后节点e.previous.next = e.next;//将该节点的后一节点的previous指向该节点的前节点//这两步就可以将该节点从链表从除去:在该链表中是无法遍历到该节点的e.next.previous = e.previous;//将该节点归空e.next = e.previous = null;e.element = null;size--;modCount++;return result;}

 其他移除方法

clear(): 从此列表中移除所有元素。

remove():获取并移除此列表的头(第一个元素)。

remove(int index):移除此列表中指定位置处的元素。

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

removeFirst():移除并返回此列表的第一个元素。

removeFirstOccurrence(Object o):从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。

removeLast():移除并返回此列表的最后一个元素。

removeLastOccurrence(Object o):从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

查找方法

对于查找方法的源码就没有什么好介绍了,无非就是迭代,比对,然后就是返回当前值。

get(int index):返回此列表中指定位置处的元素。

getFirst():返回此列表的第一个元素。

getLast():返回此列表的最后一个元素。

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

lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。

二、Queue

Queue接口:定义了队列数据结构,元素是有序的(按插入顺序),先进先出

DeQueue(Double-ended queue)接口: 继承了Queue接口,创建双向队列,灵活性更强,可以前向或后向迭代,在队头队尾均可插入或删除元素。它的两个主要实现类是ArrayDeque和LinkedList

1.ArrayDeque(底层使用循环数组实现双向队列)

创建

public ArrayDeque() {// 默认容量为16elements = new Object[16];
}public ArrayDeque(int numElements) {// 指定容量的构造函数allocateElements(numElements);
}
private void allocateElements(int numElements) {int initialCapacity = MIN_INITIAL_CAPACITY;// 最小容量为8// Find the best power of two to hold elements.// Tests "<=" because arrays aren't kept full.// 如果要分配的容量大于等于8,扩大成2的幂(是为了维护头、尾下标值);// 否则使用最小容量8if (numElements >= initialCapacity) {initialCapacity = numElements;initialCapacity |= (initialCapacity >>>  1);initialCapacity |= (initialCapacity >>>  2);initialCapacity |= (initialCapacity >>>  4);initialCapacity |= (initialCapacity >>>  8);initialCapacity |= (initialCapacity >>> 16);initialCapacity++;if (initialCapacity < 0)   // Too many elements, must back offinitialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements}elements = new Object[initialCapacity];}

代码解析(找到比输入大的,并且与输入相邻的2的次方数): 

initialCapacity |= (initialCapacity >>>  1);
initialCapacity |= (initialCapacity >>>  2);
initialCapacity |= (initialCapacity >>>  4);
initialCapacity |= (initialCapacity >>>  8);
initialCapacity |= (initialCapacity >>> 16);

运算符:

  • | :二进制运算时,有1就是1

        例:1|0=1,1|1=1,0|0=0

  • >>>:右移几位,0补位

        例:111>>>1=011
                111>>>2=001

例:

比如initialCapacity = 9,化成二进制就是:1001

 initialCapacity |= (initialCapacity >>>  1);
  • 此时initialCapacity=1001,initialCapacity先右移1位,变成0100
  • 然后0100再与之前的initialCapacity做或运算,也就是0100|1001=1101
  • 此时initialCapacity=1101
initialCapacity |= (initialCapacity >>>  2);
  • 1101>>>2=0011
  • 0011|1101=1111
  • 此时nitialCapacity=1111
initialCapacity |= (initialCapacity >>>  4);
  • 1111>>>4=0000
  • 0000|1111=1111
  • 此时nitialCapacity=1111
initialCapacity |= (initialCapacity >>>  8);
  • 1111>>>8=0000 0000
  • 0000 0000|1111,也就是0000 0000|0000 1111 = 0000 1111
  • 0000 1111也就是1111
  • 此时nitialCapacity=1111

一直右移,做或运算,最后就变成了全是1的

为什么要做initialCapacity++运算(即initialCapacity为什么+1):

        因为计算的结果都是1的,加了1之后就会进一位并且变成1000…000,变成2的次方数

        若是initialCapacity位数很多呢?位数多到,上述的最后一步>>>16都不能包括我所有的位数。

        不可能!!!

        因为initialCapacity输入类型是int型(int型最大值是2的31次方)

        而上述计算中:

  • >>>1相当于除以2的1次方
  • 然后>>>2是除以2的2次方
  • 然后>>>4——2的4次方
  • 然后>>>8——2的8次方
  • 最后>>>16——2的16次方
  • 加起来就是一共运算了2的31次方的数。

        所以输入的int型,都可以移动的到

add操作

add(E e) 调用 addLast(E e) 方法:
public void addLast(E e) {if (e == null)throw new NullPointerException("e == null");elements[tail] = e; // 根据尾索引,添加到尾端// 尾索引+1,并与数组(length - 1)进行取‘&’运算,//因为length是2的幂,所以(length-1)转换为2进制全是1,// 所以如果尾索引值 tail 小于等于(length - 1),//那么‘&’运算后仍为 tail 本身;如果刚好比(length - 1)大1时,// ‘&’运算后 tail 便为0(即回到了数组初始位置)//正是通过与(length - 1)进行取‘&’运算来实现数组的双向循环。// 如果尾索引和头索引重合了,说明数组满了,进行扩容。if ((tail = (tail + 1) & (elements.length - 1)) == head)doubleCapacity();// 扩容为原来的2倍
}addFirst(E e) 的实现:
public void addFirst(E e) {if (e == null)throw new NullPointerException("e == null");// 此处如果head为0,//则-1(1111 1111 1111 1111 1111 1111 1111 1111)与(length - 1)//进行取‘&’运算,结果必然是(length - 1),即回到了数组的尾部。elements[head = (head - 1) & (elements.length - 1)] = e;// 如果尾索引和头索引重合了,说明数组满了,进行扩容if (head == tail)doubleCapacity();
}

remove操作

remove()方法最终都会调对应的poll()方法:public E poll() {return pollFirst();}public E pollFirst() {int h = head;@SuppressWarnings("unchecked") E result = (E) elements[h];// Element is null if deque emptyif (result == null)return null;elements[h] = null;     // Must null out slot// 头索引 + 1head = (h + 1) & (elements.length - 1);return result;}public E pollLast() {// 尾索引 - 1int t = (tail - 1) & (elements.length - 1);@SuppressWarnings("unchecked") E result = (E) elements[t];if (result == null)return null;elements[t] = null;tail = t;return result;}

2.PriorityQueue(底层用数组实现堆的结构)

Queue与PriorityQueue区别:

  • Queue是一种遵循FIFO规则的队列,拿数据的时候按照加入队列的顺序拿取
  • PriorityQueue每次拿数据的时候都会拿出优先级最高的数据

PriorityQueue原理:

  • 优先队列内部维护着一个堆,每次取数据的时候都从堆顶拿数据(堆顶的优先级最高)

add添加方法

public boolean add(E e) {return offer(e); // add方法内部调用offer方法
}
public boolean offer(E e) {if (e == null) // 元素为空的话,抛出NullPointerException异常throw new NullPointerException();modCount++;int i = size;if (i >= queue.length) // 如果当前用堆表示的数组已经满了,调用grow方法扩容grow(i + 1); // 扩容size = i + 1; // 元素个数+1if (i == 0) // 堆还没有元素的情况queue[0] = e; // 直接给堆顶赋值元素else // 堆中已有元素的情况// 重新调整堆,从下往上调整,因为新增元素是加到最后一个叶子节点siftUp(i, e); return true;
}
private void siftUp(int k, E x) {if (comparator != null)  // 比较器存在的情况下siftUpUsingComparator(k, x); // 使用比较器调整else // 比较器不存在的情况下siftUpComparable(k, x); // 使用元素自身的比较器调整
}
private void siftUpUsingComparator(int k, E x) {while (k > 0) { // 一直循环直到父节点还存在int parent = (k - 1) >>> 1; // 找到父节点索引,等同于(k - 1)/ 2Object e = queue[parent]; // 获得父节点元素// 新元素与父元素进行比较,如果满足比较器结果,直接跳出//否则进行调整if (comparator.compare(x, (E) e) >= 0) break;queue[k] = e; // 进行调整,新位置的元素变成了父元素k = parent; // 新位置索引变成父元素索引,进行递归操作}queue[k] = x; // 新添加的元素添加到堆中
}

原理图:

poll出队方法:

public E poll() {if (size == 0)return null;int s = --size; // 元素个数-1modCount++;E result = (E) queue[0]; // 得到堆顶元素E x = (E) queue[s]; // 最后一个叶子节点queue[s] = null; // 最后1个叶子节点置空if (s != 0)siftDown(0, x); // 从上往下调整,因为删除元素是删除堆顶的元素return result;
}
private void siftDown(int k, E x) {if (comparator != null) // 比较器存在的情况下siftDownUsingComparator(k, x); // 使用比较器调整else // 比较器不存在的情况下siftDownComparable(k, x); // 使用元素自身的比较器调整
}
private void siftDownUsingComparator(int k, E x) {int half = size >>> 1; // 只需循环节点个数的一般即可while (k < half) {int child = (k << 1) + 1; // 得到父节点的左子节点索引,即(k * 2)+ 1Object c = queue[child]; // 得到左子元素int right = child + 1; // 得到父节点的右子节点索引if (right < size &&// 左子节点跟右子节点比较,取更大的值comparator.compare((E) c, (E) queue[right]) > 0)c = queue[child = right];// 然后这个更大的值跟最后一个叶子节点比较if (comparator.compare(x, (E) c) <= 0)  break;queue[k] = c; // 新位置使用更大的值k = child; // 新位置索引变成子元素索引,进行递归操作}queue[k] = x; // 最后一个叶子节点添加到合适的位置
}

remove删除队列元素:

public boolean remove(Object o) {int i = indexOf(o); // 找到数据对应的索引if (i == -1) // 不存在的话返回falsereturn false;else { // 存在的话调用removeAt方法,返回trueremoveAt(i);return true;}
}
private E removeAt(int i) {modCount++;int s = --size; // 元素个数-1if (s == i) // 如果是删除最后一个叶子节点queue[i] = null; // 直接置空,删除即可,堆还是保持特质,不需要调整else { // 如果是删除的不是最后一个叶子节点E moved = (E) queue[s]; // 获得最后1个叶子节点元素queue[s] = null; // 最后1个叶子节点置空siftDown(i, moved); // 从上往下调整if (queue[i] == moved) { // 如果从上往下调整完毕之后发现元素位置没变,从下往上调整siftUp(i, moved); // 从下往上调整if (queue[i] != moved)return moved;}}return null;
}

siftDown() 下滤过程:

siftUp() 上滤过程:

 四、总结:

  1. jdk内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。
  2. PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。

参考文章:https://github.com/h2pl/Java-Tutorial/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E8%AF%A6%E8%A7%A32%EF%BC%9AQueue%E5%92%8CLinkedList.md

n |= n >>> 1——JDK10的HashMap原理 tableSizeFor(initialCapacity)方法_太书红叶的博客-CSDN博客

Java基础汇总(十四)——LinkedList,Queue相关推荐

  1. java基础第十四天_IO

    1.定义函数,输出一个byte的二进制字符串. 2.定义工具类,完成int数和byte[]之间的相互转换. 3.阐述IO流. 输入输出流 字符字节流 缓冲和非缓冲流 转换流. 4.通过File对象打印 ...

  2. 重学java基础第二十四课:标识符合关键字

  3. 重学java基础第十四课:java特性和优势

  4. Java SE基础(十四)常用API

    Java SE基础(十四)常用API 概述 Object类 构造方法 成员方法 toString()方法 equals(Object obj)方法 Arrays排序 Arrays类概述 Arrays成 ...

  5. go设置后端启动_Go语言基础(十四)

    Go语言基础(十四) 一.Redis 二.NSQ 三.Go module 四.Context......0 一.Redis Redis是一个key-value存储系统.和Memcached类似,它支持 ...

  6. Java基础汇总(三)——final

    一.final 变量 定义: 成员变量:在类中,方法外 本地变量(方法内的局部变量):方法中或者方法声明上(形式参数) 区别: 位置不同:成员变量在类中,方法外:局部变量在方法中或者方法声明上(形式参 ...

  7. 2021-11-23----韩顺平Java入门第十四天

    韩顺平Java入门第十四天(排序.查找) 1.排序     1).排序的介绍:排序是将多个数据,依指定的顺序进行排列的过程.     2).内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序 ...

  8. JAVA基础知识(四)面向对象基础

    文章目录 JAVA基础知识(四)面向对象基础 1. 类 2. 访问权限修饰符 3. `static`修饰符 3.1 静态代码块 4. `new`一个对象 5. 方法 5.1 `this` 5.2 方法 ...

  9. Java面试题(十四)----- 如何获取父类的类名字 ? 如何获取当前运行类的类名

    Java面试题(十四)----- 如何获取父类的类名字 ? 如何获取当前运行类的类名 文章目录

  10. Java入门基础(十四)Maven仓库的安装

    文章目录 一.下载配置Maven环境 1.下载maven.jar解压缩,放在一个文件夹下 2.点击此电脑=>属性=>高级设置=>环境变量=>系统变量=>新建 3.变量名为 ...

最新文章

  1. 模式设计概述:代理者模式
  2. 史上最全《知识图谱》2020综述论文!!!
  3. Android Annotations配置与使用
  4. .net随笔-vb.net Accord.Net机器学习之SVM分类
  5. 《学习之道》第七章总结
  6. 【MySQL】PREPARE 的应用
  7. 骁龙8性能巅峰旗舰!黑鲨5系列获3C认证:配备120W快充
  8. python检测文件的MD5值
  9. 卡拉曼达聊天服务器断开修复也没用,英雄联盟新版客户端 聊天系统神似QQ
  10. 03-树3 Tree Traversals Again【建树遍历】
  11. 如何把 Mac 中的文件拷贝到NTFS硬盘?
  12. html图片自动切换的幻灯片效果的,JS实现可切换图片的幻灯切换效果示例
  13. linux删除用户账户_如何在Linux中删除删除用户帐户?
  14. 身份证前两位对照省份表
  15. 一文读懂 MySQL 事务隔离机制
  16. 计算机基础知识初中生学习,初中生具备了学习电脑的能力吗?
  17. 以太坊geth节点同步亲测经历
  18. 个人开发者如何免费打造属于自己的微信小程序
  19. 【精讲】vue数据绑定、数据内容的应用、数据结构、数据代理
  20. App中的微信支付和支付宝支付如何实现

热门文章

  1. 做刀尖上的舞者 京东无线服务端的三次架构演进
  2. 产品项目分析之竞品分析
  3. 便携式CAN分析仪can总线的产品特性及选型分享
  4. CPU与Cache、内存以及硬盘之间的数据交换
  5. 事业编招聘:国家无线电监测中心2022年公开招聘
  6. Java IO流以及File类
  7. TCGA数据库的学习-系列一
  8. Jenkins+Gitlab+SonarQube 代码质量管理集成
  9. 洛谷 P3964 松鼠聚会
  10. 计算点在线上的投影坐标