LinkedList 是 Java 中 java.util 包提供的一个类,它实现了 List 接口,并且提供了双向链表的结构,维护了长度、头节点和尾节点,此外,还实现了 Deque 和 Queue 接口,可以按照队列、栈和双端队列的方式进行操作
LinkedList 的特点如下:
- 按需分配空间,不需要预先分配很多空间。
- 不可以随机访问,按照索引访问效率比较低,必须从头或尾顺着链表查找,时间复杂度为 。
- 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,时间复杂度为 。
- 在两端添加、删除元素的效率很高,时间复杂度为 。
- 在中间插入、删除元素,要先定位,效率比较低,时间复杂度为 ,但修改本身的效率很高,时间复杂度为
用法#
LinkedList 的构造方法与 ArrayList 类似,有两个:一个是默认构造方法,另外一个可以接受一个已有的 Collection,如下所示:
public LinkedList();
public LinkedList(Collection<? extends E> c);java可以这样创建 LinkedList
List<Integer> list1 = new LinkedList<>();
List<String> list2 = new LinkedList<>(
Arrays.asList(new String[] {"a", "b", "c"}));javaLinkedList 与 ArrayList 一样,同样实现了 List 接口,而 List 接口扩展了 Collection 接口,Collection 又扩展了 Iterable 接口,所有这些接口的方法都是可以使用的。
队列#
此外,LinkedList 还实现了队列 Queue 接口,支持先进先出,在尾部添加元素,从头部删除元素,Queue 接口的定义为:
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}javaQueue 接口继承自 Collection 接口,主要操作有三类:
-
在尾部添加元素
add方法,队列为满时会抛出IllegalStateException异常offer方法,队列为满时只是返回false
-
返回头部元素,但不改变队列
element方法,队列为空时抛出NoSuchElementException异常peek方法,队列为空时返回null
-
返回头部元素,并且从队列中删除
remove方法,队列为空时抛出NoSuchElementException异常poll方法,队列为空时返回null
把 LinkedList 当作队列使用:
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
while(queue.peek() != null)
System.out.println(queue.poll());java栈#
Java 中没有单独的栈接口,栈相关方法放在了表示双端队列的接口 Deque 中,Deque 接口提供了很多方法,栈相关的主要有三个:
void push(E e);
E pop();
E peek();javapush方法表示入栈,如果栈满会抛出IllegalStateException异常pop方法表示出栈,如果栈空会抛出NoSuchElementException异常peek方法查看栈顶元素,如果栈为空返回null
把 LinkList 当栈使用:
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
while(stack.peek() != null) {
System.out.println(stack.pop());
}java双端队列#
还一个更为通用的操作两端的接口 Deque,Deque 扩展了 Queue,除了栈的操作方法,还有如下更为明确的操作两端的方法:
public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
// *** Stack methods ***
void push(E e);
E pop();
boolean remove(Object o);
boolean contains(Object o);
public int size();
Iterator<E> iterator();
Iterator<E> descendingIterator();
}
javaxxxFirst 方法操作头部,xxxLast 方法操作尾部,每种操作有两种形式:
- 队列为空时
getXXX/removeXXX会抛出异常,而peekXXX/pollXXX会返回null; - 队满时
addXXX会抛出异常,offerXXXX只是返回false。
Deque 接口还有个迭代器方法,可以从后往前遍历
Iterator<E> descendingIterator();java例如
public class Main {
public static void main(String[] args) {
Deque<Integer> dq = new LinkedList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
Iterator<Integer> iterator = dq.descendingIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next());
}
}
}java输出
10987654321java实现原理#
LinkedList 的内部组成#
LinkedList 的内部实现是双向链表,每个元素在内存中单独存放,通过链接连在一起
为了表示链接的关系,需要一个节点的概念,节点内部包括实际存放的元素和两个指针,分别指向前一个节点(前驱)和后一个节点(后继)。节点是 LinkedList 的一个静态内部类:
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;
}
}javaNode 类表示节点,item 指向实际的元素,next 指向后一个节点,prev 指向前一个节点
LinkedList 内部组成就是如下三个实例变量:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;javasize 表示链表长度,默认为 0,first 指向链表的头节点,last 指向链表的尾节点,初始值都为 null
构造函数#
LinkedList 有两个构造函数
public LinkedList();
public LinkedList(Collection<? extends E> c);java无参构造函数是空的,实例变量全使用默认值
public LinkedList() {
}java另外一个构造函数调用 addAll 方法将传入的集合元素加入到链表中
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}javaaddAll 方法调用了重载的 addAll(int index, Collection<? extends E> c) 将参数 size 作为插入位置的索引
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}java该方法首先调用 checkPositionIndex 方法检查 index >=0 && index <= size,如果不是会抛出 IndexOutOfBoundsException 异常
然后将传入的集合转换成 Object 数组,计算数组的长度,如果数组长度为 0 说明没有元素可以插入,返回 false
声明两个指针,pred 指向插入位置的前一个节点,succ 指向插入位置。
如果 index == size 说明插入位置在链表末尾,succ=null,pred 指向链表的尾部 last
否则需要调用 node() 方法查找插入位置的节点,node 方法中会先判断 index 是在链表的前半部分还是后半部分,然后决定是从前到后遍历还是从后往前遍历去查找节点。找到 succ 节点后,pred = succ.prev
然后遍历数组,创建新节点,将 pred 作为新节点的前驱,如果前驱为 null,则说明链表为空,新节点就是头节点,将 first 指向新节点,否则 pred.next = newNode,再下一轮循环中 pred 为 newNode,会给 pred.next 赋值,所以创建新节点的时候,newNode.next 的传参为 null
插入完成后如果 succ 为 null 说明插入之前链表为空,尾节点就是插入的最后一个节点,所以 last = pred,否则需要将 pred 的后继指向 succ,succ 的前驱指向 pred
最后修改 size 和 modCount 的值
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;
}
Node<E> node(int index) {
// assert isElementIndex(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;
}
}java添加元素 add#
add 方法用于向链表中添加元素,有两个重载
第一个 add(E e) 是向链表尾部插入元素,主要调用的是 linkLast(e) 方法
public boolean add(E e) {
linkLast(e);
return true;
}javalinkLast 方法首先创建一个新节点 newNode。l 和 last 指向原来的尾节点,如果原来链表为空,则为 null。
然后修改尾节点 last 指向新的节点 newNode,并修改前驱节点的后向指针,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点
最后修改 size 和 modCount 的值
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++;
}javaadd(int index, E element) 方法则是在链表指定位置插入元素,首先检查插入位置的合法性
然后如果 index == size 说明是在链表尾部插入,直接调用 linkLast(element); 方法
否则调用 linkBefore(element, node(index)) 方法,并调用 node 方法获取插入位置的节点
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}javalinkBefore 方法的实现与 addAll 中大同小异
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++;
}java根据索引访问元素 get#
LinkedList 提供了 get(int index) 方法根据索引访问元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}java首先调用 checkElementIndex 方法检查索引位置的有效性
然后调用 node 方法返回索引位置的节点,获取节点的 item 值即可
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}java根据内容查找元素 indexOf#
LinkedList 提供了 indexOf(Object o) 方法根据内容查找元素,代码也很简单,从头节点顺着链接后找,如果要找的是 null,则找第一个 item 为 null 的节点,否则使用 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;
}java删除元素 remove#
remove(int index) 方法用于删除指定位置的元素,通过 node 方法找到节点后,调用了 unlink 方法
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}javaunlink 方法的代码如下:
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;
}java删除x节点,基本思路就是让x的前驱和后继直接链接起来,next是x的后继,prev是x的前驱,具体分为两步:
1)让x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
2)让x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。