Java基础汇总(十四)——LinkedList,Queue
一、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() 上滤过程:
四、总结:
- jdk内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。
- 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相关推荐
- java基础第十四天_IO
1.定义函数,输出一个byte的二进制字符串. 2.定义工具类,完成int数和byte[]之间的相互转换. 3.阐述IO流. 输入输出流 字符字节流 缓冲和非缓冲流 转换流. 4.通过File对象打印 ...
- 重学java基础第二十四课:标识符合关键字
- 重学java基础第十四课:java特性和优势
- Java SE基础(十四)常用API
Java SE基础(十四)常用API 概述 Object类 构造方法 成员方法 toString()方法 equals(Object obj)方法 Arrays排序 Arrays类概述 Arrays成 ...
- go设置后端启动_Go语言基础(十四)
Go语言基础(十四) 一.Redis 二.NSQ 三.Go module 四.Context......0 一.Redis Redis是一个key-value存储系统.和Memcached类似,它支持 ...
- Java基础汇总(三)——final
一.final 变量 定义: 成员变量:在类中,方法外 本地变量(方法内的局部变量):方法中或者方法声明上(形式参数) 区别: 位置不同:成员变量在类中,方法外:局部变量在方法中或者方法声明上(形式参 ...
- 2021-11-23----韩顺平Java入门第十四天
韩顺平Java入门第十四天(排序.查找) 1.排序 1).排序的介绍:排序是将多个数据,依指定的顺序进行排列的过程. 2).内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序 ...
- JAVA基础知识(四)面向对象基础
文章目录 JAVA基础知识(四)面向对象基础 1. 类 2. 访问权限修饰符 3. `static`修饰符 3.1 静态代码块 4. `new`一个对象 5. 方法 5.1 `this` 5.2 方法 ...
- Java面试题(十四)----- 如何获取父类的类名字 ? 如何获取当前运行类的类名
Java面试题(十四)----- 如何获取父类的类名字 ? 如何获取当前运行类的类名 文章目录
- Java入门基础(十四)Maven仓库的安装
文章目录 一.下载配置Maven环境 1.下载maven.jar解压缩,放在一个文件夹下 2.点击此电脑=>属性=>高级设置=>环境变量=>系统变量=>新建 3.变量名为 ...
最新文章
- 模式设计概述:代理者模式
- 史上最全《知识图谱》2020综述论文!!!
- Android Annotations配置与使用
- .net随笔-vb.net Accord.Net机器学习之SVM分类
- 《学习之道》第七章总结
- 【MySQL】PREPARE 的应用
- 骁龙8性能巅峰旗舰!黑鲨5系列获3C认证:配备120W快充
- python检测文件的MD5值
- 卡拉曼达聊天服务器断开修复也没用,英雄联盟新版客户端 聊天系统神似QQ
- 03-树3 Tree Traversals Again【建树遍历】
- 如何把 Mac 中的文件拷贝到NTFS硬盘?
- html图片自动切换的幻灯片效果的,JS实现可切换图片的幻灯切换效果示例
- linux删除用户账户_如何在Linux中删除删除用户帐户?
- 身份证前两位对照省份表
- 一文读懂 MySQL 事务隔离机制
- 计算机基础知识初中生学习,初中生具备了学习电脑的能力吗?
- 以太坊geth节点同步亲测经历
- 个人开发者如何免费打造属于自己的微信小程序
- 【精讲】vue数据绑定、数据内容的应用、数据结构、数据代理
- App中的微信支付和支付宝支付如何实现