Author : lss 路漫漫其修远兮,不至于代码
上次我们就提到了过了,LinkedList 即是一个顺序容器,也是一个队列 (queue) 和 栈 (Stack), 本节将对 LinkedList 的队列和栈, 进行刨析。
上个章节已经说过,LinkedList 不仅实现了 List 同时 实现了 Deque 。而 Deque 又实现了 Queue。Queue这个接口不仅继承了来自 Collection 接口的方法之外,同时还提供了一些额外方法。这里分为了两组。一组以抛异常的形式(无数据会抛出异常),另一组以返回值实现(无数据返回 null )
Throws exception | Return value | |
---|---|---|
insert (添加) | add ( e ) | offer ( e ) |
Remove(移除) | remove | poll() |
Examine(获取) | element() | peek() |
说完这个 Queue 后 再来看下 Deque 这个接口。 Deque( double ended queue)表示双向队列。由于是双向队列,所以需要对列队的头部和尾部都进行操作。它同时也支持了两种实现方式 一种以抛异常形式,另一种为返回值形式。
Deque Method | Description |
---|---|
addLast (e) | 队尾添加 |
offerLast (e) / offreFirst | 队尾添加 |
removeFirst() / removeLast | 获取并删除首元素 |
pollFirst() / pollLast | 获取并删除首元素 |
getFirst() / getLast | 获取但不删除栈顶元素 |
peekFirst() / peekLast | 获取但不删除栈顶元素 |
// 因为是队列,所以我们的重点只关心头节点数据。
// 获取方法
public E peek() {
final Node<E> f = first;
// 头节点为null 返回 null : 返回该节点数据
return (f == null) ? null : f.item;
}
// 都是获取头节点的数据方法 唯一区别:这个方法会抛出异常
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
// 没有数据会抛出异常
throw new NoSuchElementException();
return f.item;
}
// 删除方法
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
// 若头节点存在数据,将头节点数据,指针置null,将 first 改为后驱节点。 如果后驱节点为null,则都设置为 null。
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
// 获取当前节点数据
final E element = f.item;
// 获取当前节点的后驱节点
final Node<E> next = f.next;
// 将当前节点数据 置 null
f.item = null;
// 将当前节点的后驱节点 置 null
f.next = null; // help GC
// 头节点指向 当前节点的后驱节点
first = next;
// 后驱节点为 null
if (next == null)
last = null;
else
// 后驱节点的前驱节点为 null
next.prev = null;
size--;
modCount++;
return element;
}
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
// 上面已有解析
return unlinkFirst(f);
}
// 添加
public boolean offer(E e) {
// 其实就是一个正常插入方法 上期已经讲过
return add(e);
}
// 队尾加入元素
public void addLast(E e) {
// 上期已经解析,不再多说
linkLast(e);
}
// 队尾添加
public boolean offerLast(E e) {
// 也就是正常插入 不在多说。
addLast(e);
return true;
}
// 头部添加
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
// 头部添加 和 尾部添加差不多 。
private void linkFirst(E e) {
// 头节点
final Node<E> f = first;
// 将插入的值包装成 Node
final Node<E> newNode = new Node<>(null, e, f);
// 头节点为 当前插入的节点
first = newNode;
if (f == null)
// 此时只有一个数据 ,既是头节点,也是尾节点
last = newNode;
else
// 将之前的头节的前驱改为 当前插入的节点。
f.prev = newNode;
size++;
modCount++;
}
// 删除头节点
// 这里调用的方法 之前都有讲解。所以没有做过多解析
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);
}
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
// 获取
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;
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E element() {
return getFirst();
}
至此,已经把两种实现方式都展示出来了。这也就是LinkedList 作为队列的一种操作。那接下来看看栈的一些操作吧。
// 添加。 也就是进栈
public void push(E e) {
addFirst(e);
}
// 上面有讲过。所以不写了哦。
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
// 获取并删除
public E pop() {
return removeFirst();
}
// 这里也就是我们所说的出栈。 将头节点数据取出,再将头节点改为取出节点的后驱节点
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
// 上面都有哦。。
return unlinkFirst(f);
}
//获取但不删除元素
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
至此,LinkedList 中的东西多多少少都已经阐述完了。
总结
: LinkedList 既是一个顺序容器 也是一个队列 和 栈。底层是由双向链表组成。不仅实现了 List 还实现了 Deque。 同时Deque 实现了 Queue 。 这两个接口都分别提供了添加,获取,删除。 等操作。分别以两种方式实现。一种以抛异常,另一种为返回值类型。 再作为栈的时候,只有一种方式实现,即抛异常的形式。
上期我们说到过一个东西。不知你还是否记得。 在使用 队列和栈的时候不推荐使用 LinkeList 而是推荐使用ArrayDeque。 从直意上看,是一个双向数组,也就是说数组的两端都可以进行添加和删除元素的操作。即循环数组 Java 中并没有这样的数组。因为Java中的数组是线性的,这里的循环数组表面看是一个数组,但是在逻辑上。他没有数组的固定开头和结尾。即可以在头部添加数据,也可以在尾部添加数据,不需要大面积的挪动数据。
先来看一副图吧,从中发现这个循环数组中多了两个指针 head, tail head指针指向了第一个有效的元素,tail指向的是尾端可插入的元素位置。 因为这个数组是循环的所以。 head 不一定是 0 tail 也不一定比 head 大。ArrayDeque 是不允许 null 存储。
//添加
public void addFirst(E e) {
if (e == null)//不允许放入null
throw new NullPointerException();
// 下标是否越界
// 假设当前在插入一个元素后 head 为0 在调用一次这个方法, 这个时候head 就为 -1 了。
// 这里代表是如果是 -1 的话就对这个数组长度取补数 这里计算head 越界只会是 -1,
// 若有不懂请自行研究二进制
elements[head = (head - 1) & (elements.length - 1)] = e;
// 当两个指针相撞 代表容量已满
if (head == tail)
//扩容 这里是进行的 2 倍的扩容
doubleCapacity();
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
// 赋值
elements[tail] = e;
// 下标是否越界。 这里和上面的 其实差不多。
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
//扩容
doubleCapacity();
}
public E pollFirst() {
E result = elements[head];
if (result == null)//null值意味着deque为空
return null;
elements[h] = null;//let GC work
head = (head + 1) & (elements.length - 1);//下标越界处理
return result;
}
public E pollLast() {
//计算tail 的上一个位置
int t = (tail - 1) & (elements.length - 1);
E result = elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
public E peekFirst() {
return elements[head]; // elements[head] is null if deque empty
}
public E peekLast() {
return elements[(tail - 1) & (elements.length - 1)];
}
总结
: ArrayDeque 和 LinkedList 性能差距 ? 为什么选择 ArrayDeque ?
ArrayDeque 底层循环数组实现,不容置疑,查询效率肯定高于 LinkedList。
循环数组不会存在删除数据后需要挪动后面的数据。所以摒弃了 数组 删除效率慢的问题
public E peekFirst() {
return elements[head]; // elements[head] is null if deque empty
}
public E peekLast() {
return elements[(tail - 1) & (elements.length - 1)];
}
总结
: ArrayDeque 和 LinkedList 性能差距 ? 为什么选择 ArrayDeque ?
ArrayDeque 底层循环数组实现,不容置疑,查询效率肯定高于 LinkedList。
循环数组不会存在删除数据后需要挪动后面的数据。所以摒弃了 数组 删除效率慢的问题