LinkedList
基于 JDK 1.8。
ArrayList 随机访问效率很高,但插入和删除性能比较低,而 LinkedList 相反,它的继承关系如下:
它和 ArrayList 同样实现了 List 接口,而 List 接口扩展了 Collection 接口,Collection 接口又扩展了 Iterable 接口。但又多实现了 Deque 接口,Deque 接口拓展了 Queue 接口,而 Queue 接口又拓展了 Collection 接口。
实现的接口
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();
}
Queue 接口扩展了 Collection 接口,它的主要操作有三个:
- 在尾部添加元素 (add, offer)
- 查看头部元素 (element, peek),返回头部元素,但不改变队列
- 删除头部元素 (remove, poll),返回头部元素,并且从队列中删除
每种操作都有两种形式,区别在于,对于特殊情况的处理不同。特殊情况是指,队列为空或者队列为满,为空容易理解,为满是指队列有长度大小限制,而且已经占满了。LinkedList 的实现中,队列长度没有限制,但别的Queue 的实现可能有。
在队列为空时,
- element 和 remove 会抛出异常 NoSuchElementException
- peek 和 poll 返回特殊值 null
在队列为满时,
- add 会抛出异常 IllegalStateException
- offer 只是返回 false
把 LinkedList 当做 Queue 使用也很简单,比如,可以这样:
Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
while(queue.peek()!=null){
System.out.println(queue.poll());
}
输出为:
a
b
c
Stack(没有单独的栈接口)
栈是一种常用的数据结构,与队列相反,它的特点是先进后出、后进先出。
Java 中有一个类 Stack,用于表示栈,但这个类已经过时了,Java 中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口 Deque 中,主要有三个方法:
void push(E e)
E pop()
E peek()
- push 表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push 会抛出异常 IllegalStateException
- pop 表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出异常 NoSuchElementException
- peek 查看栈头部元素,不修改栈,如果栈为空,返回 null
Deque<String> stack = new LinkedList<>();
stack.push("a");
stack.push("b");
stack.push("c");
while(stack.peek()!=null){
System.out.println(stack.pop());
}
输出为:
c
b
a
Deque
Deque 接口定义如下:
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 ***
//...
// *** Stack methods ***
//...
// *** Collection methods ***
//...
}
在 Deque 接口中,有 Queue 方法与 Stack 方法以及 Collection 方法。
xxxFirst操作头部,xxxLast操作尾部。
与队列类似,每种操作有两种形式,区别也是在队列为空或满时,处理不同。
队列为空时,getXXX / removeXXX 会抛出异常,而 peekXXX / pollXXX 会返回 null。
为满时,addXXX 会抛出异常,offerXXX只是返回 false。
栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰。
Deque 接口还有一个迭代器方法,其注释为 Returns an iterator over the elements in this deque in reverse sequential order. The elements will be returned in order from last (tail) to first (head).:
Iterator descendingIterator();
例子:
Deque<String> deque = new LinkedList<>(
Arrays.asList(new String[]{"a","b","c"}));
Iterator<String> it = deque.descendingIterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
输出为
c b a
用法小结
LinkedList 的用法是比较简单的,与 ArrayList 用法类似,支持 List 接口,只是,LinkedList 增加了一个接口 Deque,可以把它看做队列、栈、双端队列,方便的在两端进行操作。
构造函数
构造函数:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
例子:
List<String> list = new LinkedList<>();
List<String> list2 = new LinkedList<>(
Arrays.asList(new String[]{"a","b","c"}));
基本原理
组成
ArrayList 内部是数组,元素在内存是连续存放的,而 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;
}
}
Node 类表示节点,item 指向实际的元素,next 指向下一个节点,prev 指向前一个节点。
LinkedList 内部是由三个变量组成:
transient int size = 0;
transient Node first;
transient Node last;
size 表示链表长度,默认为 0,first 指向头节点,last 指向尾节点,初始值都为null。
Add 方法
Add(E e) 方法
方法代码如下:
public boolean add(E e) {
linkLast(e);
return true;
}
其中调用了 linkLast(E e) 方法,该方法代码如下:
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++;
}
该方法先创建一个新的节点 newNode。prev 指向原来的尾节点,后继指向 null。代码为:
Node<E> newNode = new Node<>(l, e, null);
修改尾节点 last,指向新的最后节点 newNode。代码为:
last = newNode;
修改前节点的后向链接,如果原来链表为空,则让头节点指向新节点,否则让原尾节点的 next 指向新节点。代码为:
if (l == null)
first = newNode;
else
l.next = newNode;
增加链表大小。代码为:
size++
modCount++
的目的与 ArrayList 是一样的,记录修改次数,便于迭代中间检测结构性变化。
Add(int index, E element) 方法
代码如下:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
先是调用 checkPositionIndex(int index) 方法,该方法检测下标的有效性,如果下标无效,则抛出异常 IndexOutOfBoundsException。
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,添加到最后面;而当 index 小于 size 时,插入到 index 对应的结点之前,调用 linkBefore(E e, Node succ) 方法,代码如下:
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++;
}
新建结点 newNode ,然后将前驱设为 pred,后继设为 succ,代码如下:
final Node<E> newNode = new Node<>(pred, e, succ);
然后将 succ 的前驱设为 newNode,代码如下:
succ.prev = newNode;
判断 succ 是否为头结点,如果是,则将头结点设为 newNode;如果不是,则将 succ 前驱的后继设为 newNode,代码如下:
if (pred == null)
first = newNode;
else
pred.next = newNode;
长度增加:
size++;
可以看出,在中间插入元素,LinkedList 只需按需分配内存,修改前驱和后继节点的链接,而 ArrayList 则可能需要分配很多额外空间,且移动所有后续元素。
Get 方法
LinkedList 类的 get(int index) 方法如下:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
其中 checkElementIndex(int index) 方法是也是检测下标的有效性,如果下标无效,则抛出异常 IndexOutOfBoundsException,与 checkPositionIndex(int index) 有细微的差距。
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
若下标有效,则调用 node(int index) 方法,来寻找对应的结点,其item属性就指向实际元素内容:
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;
}
}
如果索引位置在前半部分 index < (size>>1)
,则从头节点开始查找,否则,从尾节点开始查找。
ArrayList中数组元素连续存放,可以直接随机访问,而在LinkedList中,则必须从头或尾,顺着链接查找,效率比较低。
IndexOf 方法
indexOf 方法,它是从头节点顺着链接往后找,如果要找的是 null,则找第一个 item 为 null 的节点,否则使用equals 方法进行比较,找到后返回下标,如果未找到,即返回 -1。代码如下:
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;
}
Remove 方法
Remove(Object o) 方法
代码如下:
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;
}
代码很简单,找到待删除的 object 后,调用 unlink(Node x) 方法,unlink(Node x) 方法如下:
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;
}
首先先判断是否为头结点,如果是,则将头结点设为
Remove(int index) 方法
代码如下:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
先是调用 checkElementIndex(int index) 方法判断 index 是否越界,然后再调用 node(int index) 方法找到节点,接着调用 unlink(Node x) 方法,代码如下:
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;
}
删除 x 节点,基本思路就是让 x 的前驱和后继直接链接起来,next 是 x 的后继,prev 是 x 的前驱,具体分为两步:
- 第一步是让 x 的前驱的后继指向 x 的后继。如果 x 没有前驱,说明删除的是头节点,则修改头节点指向 x 的后继。
- 第二步是让 x 的后继的前驱指向 x 的前驱。如果 x 没有后继,说明删除的是尾节点,则修改尾节点指向 x 的前驱。
对于队列、栈和双端队列接口,长度可能有限制,LinkedList 实现了这些接口,不过 LinkedList 对长度并没有限制。
特点分析
LinkedList 内部是用双向链表实现的,它是一个 List ,但也实现了 Deque 接口,可以作为队列、栈和双端队列使用,维护了长度、头节点和尾节点,这决定了它有如下特点:
- 按需分配空间,不需要预先分配很多空间
- 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
- 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
- 在两端添加、删除元素的效率很高,为O(1)。
- 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。