学习内容:
- 概述
- LinkedList集合的主要结构
- LinkedList集合的添加操作
- LinkedList集合的移除操作
- LinkedList集合的查找操作
- LinkedList集合的栈工作特性
- LinkedList集合和ArrayList的对比
学习产出:
概述
学习的最后一种主要的List集合,学习的一种Queue集合。
LinkedList同时具有List集合和Queue集合的基本特征
LinkedList集合的主要结构
LinkedList集合的主要结构是双向链表,不要求节点具有连续的内存存储地址,每一个节点都使用一个LinkedList.Node类的对象进行描述
item属性:当前Node节点上存储的具体数据对象
next属性:当前节点指向的下一个节点
prev属性:当前节点指向的上一个节点
在当前结构中,双向链表的头节点的prev和尾节点next为空
LinkedList内部采用first记录头节点,使用last属性记录尾节点,使用size属性记录当前长度
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;
}
由上面的源码我们可以知道
1,当双向链表没有任何数据对象的时候,first和last一定为空
2,当只有一个数据对象的时候,first和last属性一定指向同一个节点
3,只有有一个对象,first和last就不可能为空
LinkedList集合的添加操作
从jdk1.8开始,LinkedList有三个用在链表不同位置添加新节点方法,linkFirst(E),linkLast(E),linkBefore(E,Node),但是这三个方法都不是public修饰发方法,这些方法要通过add(E),addLast(E),addFirst(E)等方法进行封装,才能提供服务
linkFirst(E)方法
向链表头部添加一个Node节点,并且调整first属性的指向位置
private void linkFirst(E e) {
//记录first属性中信息
final Node<E> f = first;
//创建一个新节点,prev为空,next为first
final Node<E> newNode = new Node<>(null, e, f);
//变更fist
first = newNode;
//如果原来的first是空的,则表明原链表中没有数据,添加的第一个数据
if (f == null)
last = newNode;
else
f.prev = newNode;
//链表长度加一
size++;
//操作次数加一
modCount++;
}
linkLast(E)方法
向链表尾部添加一个Node节点,并且调整last属性的指向位置
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++;
}
linkBefore(E)方法
在指定节点前的索引栏位上插入一个新节点,需要注意的是LinkedList集合的操作逻辑保证了srcc入参一定是不为空的
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
LinkedList集合的移除操作
复制移除的三个方法方法unlinkFirst(Node),unlinkList(Node),unlink(Node),同样这三个方法也是通过包装对外提供服务removeFirst(),removeLast(),remove(Object)
unlinkFirst(Node)方法
移除头节点,并且设置它的后续节点为头节点
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//记录头节点的值
final E element = f.item;
//记录头节点的下一个节点
final Node<E> next = f.next;
//清楚节点中的值
f.item = null;
f.next = null; // help GC
//设置头节点为第二个节点
first = next;
//如果第二个节点为空,则说明只有一个元素让尾节点也为空
if (next == null)
last = null;
else
//让新头节点的prev为空
next.prev = null;
size--;
modCount++;
return element;
}
unlinkList(Node)方法与这个类似
unlink(Node)方法
E unlink(Node<E> x) {
// assert x != null;
//记录当前节点的数据,以及前后节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//条件成立则说明是头节点
if (prev == null) {
first = next;
} else {
//让节点的上一个节点下一个节点为这个节点的下一个节点,让这个点的前引用为空
prev.next = next;
x.prev = null;
}
//成立则说明是尾节点,逻辑和头节点类似
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
LinkedList集合的查找操作
在调用linkBefore(E,Node)方法插入新节点时,和unlink(Node)方法时,都先要找到操作的这个节点。
双向链表查询指定索引位置的方式,就是从头节点或者尾节点开始遍历具体的实现如下
Node<E> node(int index) {
// assert isElementIndex(index);
//根据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;
}
}
node(int ) 方法在addAll(int,Collection),get(int),set(int,E),add(int,E),remove(int)方法等读和写的时候都被使用。
有一些方法则比较特殊indexOf(Object),lastIndexOf(Object),remove(Object)
下面是indexOf(Object)的源码
//该方法是从头节点开始查询(如果是o为空,则是查询值为空的节点,如果不是为空,则是查询用equals方法比较相等的节点)
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
LinkedList集合的栈工作特性
LinkedList既实现了List,又实现了Deque,Deque具有传统队列的特殊定义,支持队列的普遍操作(如出队入队)。而Deque是双端队列,它支持从队列的两端独立检索和添加节点,因此Deque既支持后进先出(LIFO)又支持先进先出(FIFO)
涉及的一些双端队列操作如下
方法名 | 方法说明 |
---|---|
addFirst(E) | 头部新增一个节点,与Push(E)方法相同 |
addLast(E) | 尾部新增一个节点 |
offerFirst(E) | 作用与addFirst(E)相同 |
offerLast(E) | 与addLast(E)相同 |
getFirst() | 返回当前双向链表中的头节点,没有节点会抛异常 |
getLast() | 返回当前双向链表的尾节点,没有节点会抛异常 |
peekFirst() | 返回头节点,没有返回null |
peekLast() | 返回尾节点,没有返回null |
push(E) | 作为栈结构,向栈顶添加一个节点。实际上就是双向链表头部设置一个新节点 |
pop() | 作为栈结构,移除栈顶节点,并返回,没有会抛异常。实际上就是调用的removeFirst() |
removeFirst() | 移除双向链表的头节点 |
removeLast() | 移除双向链表尾部节点 |
pollFirst() | 移除双向链表头部节点,没有返回null |
pollLast() | 移除双向链表尾部节点,没有返回null |
LinkedList集合和ArrayList的对比
两种集合写的操作性能比较
-
ArrayList
- 当使用add(E)方法在数组尾部添加新数据对象,数组都有多余容量,此时时间复杂度为O(1)
- 当使用add(E)方法在数组尾部添加新数据对象,数组没有多余容量,此时数组要先扩容,在添加,会有较多的时间消耗
- 极端情况,在使用add(int ,E)方法在第一个位置添加数据,且此时数组没有多余容量,则要先扩容,然后当前数组所有数据对象整体向后移动一个位置,此时耗时最多
-
LinkedList
- 在头尾增加数据使用addFirst(E),addLast(E),add(E),方法没有查询的消耗,时间复杂度为O(1)。
- 不是在头尾进行添加操作,则需要先找到正确的索引位置,此时时间复杂度为O(n)。
两种集合读的操作性能比较
ArrayList无论是读哪个位置的数据,时间复杂度都为O(1),对性能的消耗都是相同的,因为ArrayList支持随机访问。
LinkedList的如果不是头尾的读,那么就存在一个查询的过程,尽管做了优化,根据索引位置确定是从前往后还是从后往前,最慢的是集合中间的元素。时间复杂度为O(n)。
不同遍历方式对LinkedList集合遍历的意义
有多种方式对LinkedList进行遍历,可以使用for(;😉,for(😃,stream()方法,Iterator迭代器等多种方式。
LinkedList<String> list=new LinkedList<>();
//第一种,传统for循环
//每一次get(i)都需要从头到尾或者从尾到头遍历一次
//时间复杂度为O(n2)
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
//第二种
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
}
//第三种
list.forEach(i ->{
});
//第四种
for(String s:list){
}
//第五种
list.stream().forEach(i->{
});
第2,3,4,5本质都是迭代器方式,每一次查询能从上一次查询的位置继续进行,而不是每次从头节点或者尾节点进行查询。
下面是for(:)进行遍历时传统迭代器ListItr的实现(部分)
private class ListItr implements ListIterator<E> {
//最后一次返回的节点
private Node<E> lastReturned;
//下一次要返回的节点
private Node<E> next;
//指向下一个要返回的索引位
private int nextIndex;
//操作次数数器
private int expectedModCount = modCount;
//创建迭代器时必须传入一个开始索引位置
//ListItr类会在指定的索引位置上添加一个变量引用
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
//如果要查询的索引位置大于或者等于当前集合大小,则表示没有下一个要查询的节点了
public boolean hasNext() {
return nextIndex < size;
}
//用于查询遍历下一个节点
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
//设置最后一次返回的节点为当前节点
lastReturned = next;
//next变量指向下一个节点
next = next.next;
//记录下一个索引位置的变量
nextIndex++;
//返回当前遍历的节点信息
return lastReturned.item;
}
//如果成立,则说明在遍历时,双向链表的结构发生了变化
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
在什么场景中推荐使用LinkedList
1,集合的写规模远远大于读操作规模,并且这种写并不是在集合尾部进行的。
2,满足上一条的情况下,需要对集合进行栈结构性质的操作(单纯的栈结构操作使用ArrayDeque)。