简介:
LinkedList底层数据结构为双向链表,与ArrayList相同的是,其是list接口的实现类,意味着其存储元素是连续且可重复的。
LinkedList继承于AbstractSequentialList类
LinkedList实现了Deque接口,代表其具备双向队列属性;实现了Cloneable,代表其可克隆;实现了Serializable接口,表示其可序列化
与ArrayList相比,LinkedList是链表,无需连续存储空间,节点是可以分布在非连续空间的。其添加、删除、插入元素十分方便,只需寻找到其索引,再进行相应操作后,将链连接完整即可(头尾节点重新赋值),时间复杂度只有O(1)。
缺点是ArrayList支持随机查询,查询十分快速,而LinkedList则需遍历整个链表进行数据匹配,时间复杂度为O(n)。
LinkedList也是线程不安全的,高并发场景下可以考虑List<String> linkedList = Collections.synchronizedList(new LinkedList<String>())这样使用,或者使用ConcurrentLinkedDeque
所以使用两种list还应根据具体情况而定。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
底层数据结构:
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;
}
}
可以看到这是典型地双向链表的节点。双向链表支持从头开始和从尾开始两种遍历方式,可以更好解决查询问题。
属性:
transient int size = 0; //元素个数
transient Node<E> first; //头结点
transient Node<E> last; //尾结点
transient表示该变量在一个可序列化的类里不会被序列化。
方法:
构造方法:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
可以看到linkedList的构造方法非常简单,不会做很多操作。
对比ArrayList提供了参数为int(容量)的构造方法,是因为倘若我们知道需要大概多少存储元素,用这个容量去初始化数组,则可以尽可能地避免数组的扩容。(多次的扩容十分损耗性能)。LinkedList为链表结构,添加和删除节点十分高效,不必去设置容量造成有可能的空间浪费。
增加元素(插入元素):
//头插
public void addFirst(E e) {
linkFirst(e);
}
//尾插
public void addLast(E e) {
linkLast(e);
}
//默认add是尾插法
public boolean add(E e) {
linkLast(e);
return true;
}
可以看到链表添加元素就是头尾插入元素,我们看一下两种插入方法的代码
//头插
private void linkFirst(E e) {
//先存储未插入前的头结点
final Node<E> f = first;
//生成新节点,放入传入的元素
final Node<E> newNode = new Node<>(null, e, f);
//将新节点赋值给first,那么链表的头结点就是newNode了
first = newNode;
//原头结点为空,证明还没有添加过元素,则让新节点做头结点
if (f == null)
last = newNode;
else
//将新节点赋值给f的prev,完成链接
f.prev = newNode;
size++;
modCount++;
}
//尾插
void linkLast(E e) {
//先存储未插入前的尾结点
final Node<E> l = last;
//生成新节点,放入传入的元素
final Node<E> newNode = new Node<>(l, e, null);
//将新节点赋值给last,那么链表的尾结点就是newNode了
last = newNode;
//原尾结点为空,证明还没有添加过元素,则让新节点做头结点
if (l == null)
first = newNode;
else
//将新节点赋值给l的next,完成链接
l.next = newNode;
size++;
modCount++;//操作数加1
}
以尾插为例,首先创造一个新节点,将值存入。
以新节点作为新的尾结点,此时要判断,如果尾结点为空,则证明没有添加过结点。那么新节点=尾结点=头结点。若不为空,还需要将新节点与旧的尾结点链接起来,即将新节点赋值给尾结点的next成员。
最后size的值加1。
头插法同理。
增加元素到指定位置(插入):
public void add(int index, E element) {
//检测下标位置
checkPositionIndex(index);
//下标同size相等,直接用尾插
if (index == size)
linkLast(element);
else
//否则调用前插,即linkBefore
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
//存储succ的前置节点
final Node<E> pred = succ.prev;
//生成新节点,前置为succ的前置,后置为succ,即链接在succ前
final Node<E> newNode = new Node<>(pred, e, succ);
//将新节点赋值给succ的前置节点
succ.prev = newNode;
//succ前置节点为空,代表succ为头结点,此时新节点作为头结点
if (pred == null)
first = newNode;
else
//否则将新节点赋值给succ前置节点,完成链接
pred.next = newNode;
//长度加1
size++;
modCount++;
}
可以看到add(int index, E element)做的操作比较简单,检测下标位置,不越界的情况下。若index是最后一个元素下标+1,即size,直接用尾插linkLast方法。否则用linkBefore方法。
linkBefore思路:
向非空节点succ前插入节点。有下面几个流程:
1、首先存储succ节点的前置节点,命名为pred。
2、生成新节点,新节点前置节点设置为pred,后置节点设置为succ,那么这时候新节点两端已完成链接。
3、将新节点赋值给succ的前置节点,此时succ与新节点完成了链接。
4、判断pred的值,此时有两种情况,pred为空,则说明succ就是头结点,新节点要插在succ前,则此时将新节点设置为头结点即可。若pred不为空,那么再将新节点赋值给pred的后置节点,此时链接完成。
顺便看一下其中的checkPositionIndex方法
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
下标越界会直接抛异常。这里注意index等于size的时候。其实是没有这一元素的,这个下标是最后一个元素下标+1。
与之相对的还有检查是否是链表元素下标的方法:
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
批量添加:
//集合元素批量添加到链表后
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//集合元素批量添加到index前,这里是前插
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;
//为size的情况就是上面的方法
if (index == size) {
succ = null;
pred = last;
} else {
//通过node方法找到index下标的节点,赋值给succ
succ = node(index);
//pred作为succ的前置节点
pred = succ.prev;
}
//遍历数组a,通过循环将集合元素插入到pred后面。
//pred在循环过程中会不断改变,即他是插入的上一个节点,也就是新插入的节点的前置节点
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
//pred为空说明succ是头结点,将新节点作为头结点,这只可能在循环第一次出现。
first = newNode;
else
//新节点设置为pred的后置
pred.next = newNode;
//插入的新节点赋值给pred,pred成为下一次插入的节点的前置节点
pred = newNode;
}
//经过循环添加后,pred就是插入的最后一个节点
//倘若succ为null,则插入的最后一个节点应作为尾结点
if (succ == null) {
last = pred;
} else {
//不为null则将succ与最后一次插入的节点链接上
pred.next = succ;
succ.prev = pred;
}
//长度增加
size += numNew;
modCount++;
return true;
}
addAll(int index, Collection<? extends E> c)思路:
1、集合c转化为数组
2、若添加的下标为size,则把这些元素添加到链表后。
否则找到插入的下标index对应的节点succ以及它的前置节点pred。之后通过循环将元素一个一个生成节点,添加到pred之后。这里有两次判断。若pred为空,则说明succ是头结点,此时添加的第一个节点应作为头结点。若succ的下一个节点为空,则说明succ是尾结点,此时添加的最后一个元素应作为尾结点。之后完成链接即可。
offer、offerFirst、offerLast方法:
public boolean offer(E e) {
return add(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
索引方法:
//不提供给外部
Node<E> node(int index) {
//size >> 1即除以二。
//通过判断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;
}
}
//找第一次出现的相同节点,正向遍历
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
//元素为空则不需要用equals方法,提高性能
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;
}
//找最后一次出现的相同节点,逆向遍历
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
上面的for循环是经典的链表遍历方法。从头(尾)结点开始,通过为一个临时节点x不断赋值为上一个节点的next(prev),则x会被赋值为链表的每一个元素,达到遍历的效果。
set方法与、contain方法、size方法、clear方法:
public E set(int index, E element) {
//检测下标越界
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//indexOf方法查询失败会返回-1
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public int size() {
return size;
}
//正向遍历,循环赋空
public void clear() {
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
比较简单,没什么可说的。
删除元素:
删除头尾元素(无参remove默认删除头元素):
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
重点看unlinkFirst与unlinkLast方法实现:
private E unlinkFirst(Node<E> f) {
//记录该节点元素数值作返回值
final E element = f.item;
//记录下一个节点
final Node<E> next = f.next;
//f的值赋空帮助GC回收
f.item = null;
f.next = null;
first = next;
//说明f是尾结点,删除后已没有节点了
if (next == null)
last = null;
else
//next作头结点,前置节点需赋空
next.prev = null;
//长度减一
size--;
modCount++;
return element;
}
//与上面方法相反
private E unlinkLast(Node<E> l) {
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null;
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
删除指定下标元素:
public E remove(int index) {
//检测下标越界
checkElementIndex(index);
return unlink(node(index));
}
E unlink(Node<E> x) {
//记录x的元素值作返回值
final E element = x.item;
//记录x的前置节点与后置节点
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;
}
//赋空方便GC回收
x.item = null;
size--;
modCount++;
return element;
}
以上的方法大体的思路都是一样的。
都是需要寻找到指定的节点,保存期元素值作为返回值,记录其前后节点。然后再给要删除的节点赋空(前置后置元素值),让GC去回收。然后将其前后置节点进行链接即可。都需要判断其是否是特殊节点(头节点或尾结点)。
通过元素值删除指定节点:
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
上述方法的实现都是先遍历,通过对比元素值寻找节点,并使用unlink方法进行删除。
poll与pop方法:
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pop() {
return removeFirst();
}
查询:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
查询也较为简单,查询首尾结点直接返回其元素值。查询一般节点则用node方法遍历即可。
迭代器遍历:
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
ListItr是LinkedList的内部类,重写了迭代器,提供了更丰富地功能:
private class ListItr implements ListIterator<E>
ListIterator只能用于List及其子类型。
有add()方法,可以往链表中添加对象
可以通过nextIndex()和previousIndex()返回当前索引处的位置
可以通过hasNext()和next()往后顺序遍历,也可以通过hasPrevious()和previous()实现往前遍历
可以通过set()实现当前遍历对象的修改