文章目录
单向链表与双向链表之间进行比较
单链表:在链表中的每个结点中,只是维护一个其后面的结点的地址,可以满足一定的增加结点以及删除结点的需求;
双向链表,在每个结点中,维护的是当前结点的上一个结点的地址以及当前结点的下一个结点的地址;
单链表的局限性:
有一个需求:查找当前的结点的上一个结点是什么,此时单链表是非常难以实现的,但是此时使用双向链表是十分容易实现的;
双向链表的缺点:
由于每个结点都多维护了一个指向上一个结点的指针,所以对于系统资源的消耗耗时存在的;
Java Deque 接口 双向队列
在常规队列中,元素是从后面添加的,而从前面删除的;
但是,在双向队列中,可以从前或者后插入和删除元素;
实现Deque的类
为了使用Deque接口的功能,我们需要使用实现接口的类:
由于LinkedList 一边实现了 List 接口,另一边实现了 Deque 接口,所以同时拥有了 List 的功能以及 Deque 的功能;
也就拥有了基本的集合的增删改查功能以及 Deque 的头部尾部同时可以增删改查的功能;
ArrayDeque
LinkedList (双向链表)
个人理解:List 集合提供了基础的增删改查方法,Deque 提供了双向链表的方法,两个接口的集成就形成了 LinkedList 实现类;
LinkedList 作为 List 集合的实现的 增
public boolean add(E e)
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
// 将元素添加到集合的尾部
// 这个方法是和 addLast() 方法的结果是一样的;
// 参数 e 添加到集合中的元素
// 返回是否添加成功
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
// 作为最后一个元素进行添加
void linkLast(E e) {
// 保存最后一个元素,因为新的元素添加进来之后,最后一个元素的指针需要指向新的结点
final Node<E> l = last;
// 创建新的结点,上一个结点指向以前旧的结点 l ,将元素放置进去,,后面没有元素了,所以指向的下一个元素为 null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
// 当 l == null 的时候,发明这个 LinkedList 是一个空的双向链表,所以链表的头部指向指新加进来的这个结点;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
小结
使用默认的添加方法,使用的是添加到双向链表的尾部
public void add(int index, E element)
完成执行位置的元素的添加,按照 add(int index, E element)
的每一句代码,搞清楚中间的调用的每一个方法,这个过程就非常简单
public void add(int index, E element) {
checkPositionIndex(index);
// 添加元素的索引是 size 的时候,放置到双向链表的尾部即可
// size = length + 1;
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
return index >= 0 && index <= size;
当关于双向链表的索引是小于 0 并且超过了 size 的时候,会报错
为什么添加元素到 size 的位置是正确的?
因为 size 的位置就是双向链表中的最后一个结点的下一个结点,刚好可以放置到最后一个元素的下一个,也是满足需要的;
node(index) 返回指定位置的结点
/**
* Returns the (non-null) Node at the specified element index.
*/
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;
}
}
void linkBefore(E e, Node succ) 前面找到的执行位置的结点之前插入新的结点,完成了指定位置插入新元素;
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// 创建出来的新节点,这个新结点的前一个元素指向 succ 的 pred ;这个新节点的下一个元素指向 succ 占用了 succ 的 index 的位置,完成了按照 index 进行结点的插入操作
final Node<E> newNode = new Node<>(pred, e, succ);
// 进行新结点的链接
succ.prev = newNode;
// pred == null 说明这是插入到了双向链表的第一个元素
if (pred == null)
first = newNode;
else// pred 非空,新加点插入到了双向链表中间的某个位置了
// 进行下一个方向的链接
pred.next = newNode;
size++;
modCount++;
}
小结
找到没有在指定位置的元素之前的 index 位置的结点,使得 原来 index 的结点的前一个结点链接新的结点,新的结点链接到原来 index 的结点的下一个结点;
在新加点加入的时候,一共有死四条链接需要链接起来,在创建新节点的时候,链接好了两条,所以在上面小结中的第一句话中又链接好了两条,一共是四条链接都链接好了;
上面的元素添加的过程如下面图示:(下面的过程执行结束之后看起来就像是 index 后面的元素整体向前面移动了一样,只是看起来,因为内存中使用的是指针连起来的,不是连续空间联系起来的)
LinkedList 作为 List 集合的实现的 删
public E remove() 删除双向链表的头结点
检索并删除此列表的头部(第一个元素)。
回报:
此列表的头部
抛出:
NoSuchElementException – 如果此列表为空
自从:
1.5
public E remove() {
return removeFirst();
}
调用 removeFirst() 方法
/**
* Removes and returns the first element from this list.
*
* @return the first element from this list
* @throws NoSuchElementException if this list is empty
*/
public E removeFirst() {
// 将 first 的结点添加一份引用
final Node<E> f = first;
// 当删除的头结点是 null 的时候,返回没有这个元素的异常
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* Unlinks non-null first node f.
*/
// 将非空的头结点移除,也就是释放掉头结点
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
// 直接将头结点存放的数值置空
f.item = null;
// 直接将头结点指向下一个结点的指针置空
f.next = null; // help GC
// first 指针指向下一个结点,原来的头结点就被从双向链表中去除了,实现了删除的效果,在链表中删除一个元素的表现形式是引用置空,会有垃圾回收器进行垃圾回收,空指针会被回收的
// 原来指向头结点的指针指向第二个结点,原来的第二个结点充当现在的第一个结点;
first = next;
// 如果 next == null 说明原来双向链表中只有一个头结点,删除了头结点双向链表中没有任何元素了;所以 last == null ,双向链表中都没有任何元素了,所以最后一个自然指向了 null 此时的 first 也是指向 null 的
if (next == null)
last = null;
else // next 非空,此时的 next 是头结点了,头结点的前一个自然是 null ,老大结点前面没有任何节点了,自己就是老大
next.prev = null;
size--;
modCount++;
return element;
}
小结
使用 remove() 方法的时候,删除的是双向链表中的头部元素,和上面的直接添加元素不同,添加的时候,从双向链表的尾部添加数据,删除的元素的时候,没有指定元素,是从双向链表的头部删除元素;
public E remove(int index) 删除指定位置的元素
删除指定位置上面的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
E unlink(Node x)
总体思路:
1、index 结点前面的第一个结点与后面的第一个结点建立双向的联系;
2、将 index 结点的 prev ,next 结点置空,从双向链表中脱落下来;
3、index 结点的 item 置空,等待垃圾回收
/**
* Unlinks non-null node x.
*/
// 传递进来的参数是待删除的结点 前面使用 node() 方法找到的待删除结点
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;
// index 前面的结点与 index 后面的结点建立联系,孤立 index 结点
// prev == null 说明删除的是头结点
if (prev == null) {
// 直接移动 first 指针到达头结点后面的元素
first = next;
} else {// 删除的不是头结点 index 前面的结点与 index 后面的结点进行链接,将 index 的结点孤立,index 结点等待垃圾回收
prev.next = next;
// 待删除的 index 结点断开与前面一个结点的联系
x.prev = null;
}
// index 后面的结点与 index 前面的结点建立联系,孤立 index 结点
if (next == null) {
// last 指针指向 prev 原来的 prev 现在成为了 最后一个结点
last = prev;
} else {
// index 后面的结点与 index 前面的结点进行链接,将 index 的结点孤立,index 结点等待垃圾回收
next.prev = prev;
// 待删除的 index 结点断开与后面一个结点的联系
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
LinkedList 作为 List 集合的实现的 改
public E set(int index, E element) 修改执行结点的数据
public E set(int index, E element) {
// 检查传递进来的索引
checkElementIndex(index);
// 找到 index 结点
Node<E> x = node(index);
E oldVal = x.item;
// 新元素的赋值
x.item = element;
// 返回旧的数据
return oldVal;
}
node(int index) 返回执行索引的结点
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;
}
}
LinkedList 作为 List 集合的实现的 查
public E get(int index) 按照索引查询结点的数据
// 输出参数:结点的索引
// 返回:索引位置的结点的数据
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
node(index) 返回 index 位置的结点
这个方法在前面介绍过具体的可以在前面查看
public E getFirst() 得到双向队列中的头结点的数据
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() 得到双向链表中尾结点的数据
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
LinkedList 作为 Deque 双向队列的实现的 增删改查
Deque 继承了 Queue ,所以 Deque 拥有 Queue 的方法,Queue 的常用方法有:
Deque 与 Queue相比 的常用方法有:
因为 LinkedList 实现了 Deque 所以上面的 Queue 以及 Deque 的所有方法 LinkedList 都是拥有的;使用上面的方法,可以实现双向队列的增删改查,由于源代码和上面的源代码解读过程比较相似,并且原代码比较简单,不在赘述;可以参考下面的博客进行阅读
小结
方法名称 List | 方法的使用效果 List |
---|---|
public boolean add(E e) | 添加结点到双向链表的尾部 |
public void add(int index, E element) | 添加结点到达指定位置 |
public E remove() | 删除双向链表的头结点 |
public E remove(int index) | 删除指定位置的结点 |