一、什么是LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
根据源码我们可以看出:
- ①LinkedList继承了AbstractSequentialList,即按次序的List,也就是说不像ArrayList一样具有随机访问元素的能力,也就是说LinkedList支持按顺序访问列表中元素的操作。
- ②实现了List接口,可进行列表相关操作
- ③实现了Deque接口,可以将LinkedList当作双端队列进行使用
- ④实现Clonalbe接口,即覆盖了clone()方法,可以进行克隆操作
- ⑤实现了java.io.Serializable接口,可以进行序列化,即支持网络传输
LinkedList的本质是双向链表,与 ArrayList 相比,LinkedList 的增加和删除对操作效率更高,而查找和修改的操作效率较低。
二、LinkedList的创建
1、成员变量及数据结构
①transient int size = 0; //LinkedList存放元素个数
②first是双向链表的表头,last指向双向链表的最后一个元素
transient Node first;
transient Node last;
③Node数据结构
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
存放数据元素,prev指向上一个节点,next指向下一个节点,item为元素值
2、first和last为什么要用transist修饰?
在ArrayList中存放元素的数据结构elementData使用transisit修饰是为了节省空间,防止扩容后数组中的空数据也被序列化。
而LinkedList使用transist修饰是因为LinkedList中使用双向链表保存数据,结点中保存前驱和后继的引用。但是序列化之后前序结点和后继结点的地址都变了,所以我们应该将数据传输过去再在另一边链接为新的链表。
LinkedList的序列化与反序列化方法:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out size
s.writeInt(size);
// Write out all elements in the proper order.
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}
3、LinkedList的创建
LinkedList list = new LinkedList(); // 普通创建方法
或者
LinkedList list = new LinkedList(Collection<? extends E> c); // 使用集合创建链表
使用集合创建链表步骤
- ①调用addAll( c )方法,而addAll( c )底层调用重写方法addAll(size, c)
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
- ②addAll方法首先通过checkPositionIndex(index);判断index是否越界,如果越界报越界异常。
- ③如果Collection为空直接return false
- ④如果Collection不为空,定义两个指针pred,和succ;如果index等于size,那么说明是在链表末尾插入Collection,使得pred指向last
- ⑤如果index不等于size,那么一定小于size,使用node(index)方法获得index处的node,是pred指向该node的上一个节点
- ⑥遍历Collection中的元素,将元素一个个插入进LinkedList中。
- ⑦最后,若succ为空,说明是在链表尾部插入的,更新last,若不为空,则是在链表中间插入的,执行操作,补全链表
三、LinkedList集合的各种插入删除操作
在前面我们说过,LinkedList可以当作队列也能作为栈,所以linkedList中有许多插入删除的方法。
链表尾部插入数据操作
方法 | 返回值 |
---|---|
public boolean add(E e) | 调用linkLast()方法,调用完返回true |
public void addLast(E e) | 调用linkLast()方法,无返回值 |
public boolean offer(E e) | 调用add()方法,返回是否插入成功 |
public boolean offerLast(E e) | 调用addLast()方法,调用完返回true |
链表头部插入数据操作
方法 | 返回值 |
---|---|
public void addFirst(E e) | 调用linkFirst()方法 |
public boolean offerFirst(E e) | 调用addFirst()方法,调用完返回true |
public void push(E e) | 调用addFirst()方法 |
删除并返回链表尾部的数据 :
方法 | 返回值 |
---|---|
public E removeLast() | 判断最后一个元素是否存在,存在调用unlinkLast()方法,不存在抛NoSuchElementException()异常,并返回删除元素的值。 |
删除并返回链表头部的数据
方法 | 返回值 |
---|---|
public E poll() | 判断first是否为空,不为空的话调用unlinkFirst()方法,返回值为删除节点的数据 |
public E remove() | 调用removeFirst()方法 |
public E removeFirst() | 判断第一个元素是否存在,存在调用unlinkLast()方法,不存在抛NoSuchElementException()异常,并返回删除元素的值。 |
public E pop() | 调用removeFirst()方法 |
获取第一个元素的值
方法 | 返回值 |
---|---|
public E element() | 调用getFirst()方法,返回链表first节点的元素值 |
public E getFirst() | 判断first节点是否为空,为空的话抛出NoSuchElementException()异常,否则返回first的元素值 |
public E peek() | first为空返回null,否则返回first的元素值 |
public E peekFirst() | 与peek()方法一样 |
获取最后一个元素的值
方法 | 返回值 |
---|---|
public E getLast() | 判断last节点是否为空,为空的话抛出NoSuchElementException()异常,否则返回last的元素值 |
public E peekLast() | last为空返回null,否则返回last的元素值 |
在任意位置插入数据:add(int index, E element)
删除任意位置数据:remove(int index)
获取任意位置的数据 : get(int index)
特别注意,作为如果使用linkedList作为栈,那么以链表头部作为栈进出的位置。
四、LinkedList的遍历
在ArrayList中已经讲过,LinkedList不支持快速随机访问,所以遍历时采用迭代器遍历更快一些
要理解这个,首先要明白linkedList是一个链表,不支持随机访问,所以linkedList要得到一个元素,即get(i)的时间复杂度时o(n),所以使用以下的遍历方式,时间复杂度就会变为o(n^2)了
for (int i=0; i<size; i++) {
list.get(i);
}
而我们可以看到linkedList实现的iterator迭代器就是一个一个从first开始遍历到index大于size为止,所以获取数据的时间复杂度为o(1)
所以使用LinkedList遍历推荐使用iterator迭代器
List<Integer> list = new LinkedList<>();
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
iterator.next();
}
五、为什么官方推荐使用LinkedList作为栈
当初 JDK1.0 在开发时,可能为了快速的推出一些基本的数据结构操作,所以推出了一些比较粗糙的类。比如,Vector、Stack、Hashtable等。这些类在之前的几个版本中,性能还不怎么好,而且其中的一些方法加上了 synchronized 关键字,进一步影响了效率!
基于 Vector 实现的栈 Stack。底层实际上还是数组,所以还是存在需要扩容。Vector 是由数组实现的集合类,它包含了大量集合处理的方法。而 Stack 之所以继承 Vector,是为了复用 Vector 中的方法,来实现进栈(push)、出栈(pop)等操作。这里就是 Stack 设计不好的地方,既然只是为了实现栈,不用链表来单独实现,而是为了复用简单的方法而迫使它继承 Vector,Stack 和 Vector 本来是毫无关系的。这使得 Stack 在基于数组实现上效率受影响,另外因为继承 Vector 类,Stack 可以复用 Vector 大量方法,这使得 Stack 在设计上不严谨。
Java 提供了 Deuqe。Deque 是继承自 Queue,而 Stack 是继承自 Vector。Java 中的 Deuqe,即“double ended queue”的缩写,是 Java 中的双端队列集合类型。Deque 具备普通队列 FIFO 的功能,同时它也具备了 Stack 的 LIFO 功能,并且保留了 push 和 pop 函数,所以使用起来应该是一点障碍都没有。而LinkedList实现了Deque接口,可以作为栈来使用,至于为什么不使用ArrayDeque则和Vector一样,当动态扩容时同样需要将数据拷贝到新数组中。
总结:在java.util.stack中,栈的底层是使用数组来实现的,数组初始大小为10。每当元素个数超出数组容量就扩展为原来的2倍,将原数组中的元素拷贝到新数组中,随着数组元素的增多,这种开销也越大。