声明:本文为作者原创,请勿转载,如果转载请注明转载地址。
先来看下双向链表的删除节点、添加节点、遍历节点的过程:
添加节点
删除节点:
查询节点:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//链表的长度
transient int size = 0;
/**
* Pointer to first node.指向链表的头结点
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.指向链表的尾节点
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
//静态内部类
private static class Node<E> {
E item;
//指向前一个节点的指针
Node<E> next;
//指向后一个节点的指针
Node<E> prev;
//构造函数:prev、元素、next
Node(Node<E> prev, E element, Node<E> next) {
//节点元素
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
LinkedList提供了两个构造器,ArrayList比它多提供了一个通过设置初始化容量来初始化类。LinkedList不提供该方法的原因:因为LinkedList底层是通过链表实现的,每当有新元素添加进来的时候,都是通过链接新的节点实现的,也就是说它的容量是随着元素的个数的变化而动态变化的。而ArrayList底层是通过数组来存储新添加的元素的,所以我们可以为ArrayList设置初始容量(实际设置的数组的大小)。
//空参数构造函数
public LinkedList() {
}
//传入一个集合(Collection)作为参数初始化LinkedList。
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the specified
* collection's iterator.
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//判断传入参数index是否合法(0<=index<=size)
checkPositionIndex(index);
//将传入的集合转换为数组
Object[] a = c.toArray();
//计算数组的长度
int numNew = a.length;
if (numNew == 0)
return false;
//1、下面的作用是定义两个Node变量pred、succ,并分情况为这两个变量初始化
//定义两个节点指针,一个pred指向待插入位置处的前一个节点,一个succ指向待插入位置
Node<E> pred, succ;
//如果index=size,说明要插入的位置就是链表的尾部
if (index == size) {
//初始化succ=null,代表要插入的位置为null
succ = null;
//初始化pred=last,pred指向last,代表要插入的位置的前一个节点应用为last
pred = last;
//否则说明要插入的位置在链表的中间位置
} else {
//初始化succ=node(index),node(index)作用是返回index位置处的节点,让succ指向该节点
succ = node(index);
//初始化pred指向succ.prev,pred指向succ指向节点的前一个节点
pred = succ.prev;
}
//2、上面已经完成了节点变量的初始化,下面是遍历集合中的每个元素,插入到链表中
//遍历数组
for (Object o : a) {
//为每次遍历到的元素创建一个节点
@SuppressWarnings("unchecked") E e = (E) o;
//创建一个节点,让该节点的prev指向pred,让该节点的next指向null
Node<E> newNode = new Node<>(pred, e, null);
//如果pred为null,说明链表为null,就让当前节点作为头结点,让first指向该头结点
if (pred == null)
first = newNode;
//否则,就让遍历到的节点元素插入到pred的后面
else
pred.next = newNode;
//pred指针向后移动
pred = newNode;
}
//3、下面要做的就是将链表连接起来,即让pred.next=succ、succ.prev=pred
//如果要插入的位置为null,说明要插入的位置为节点的最后一个位置
if (succ == null) {
//让last指针指向pred
last = pred;
} else {
//否则让pred.next指向succ
pred.next = succ;
//让succ的prev指向pred
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
总结:
(1) 先将传入的集合转换为数组
(2) 定义两个变量pred、succ并初始化,其中succ指向要插入的节点,pred指向要插入节点的前驱节点;如果要插入的节点为链表的尾部,就让pred指向last,succ指向null
。如果要插入的节点不在链表的尾部,就让succ指向要插入的节点,pred指向succ.prev
。之所以定义这两个节点变量一是为了遍历链表而是为了防止链表断掉。
(3) 遍历集合,每次遍历将集合中的元素创建为节点,该newCode.prev指向perd
,next指向nul
l。如果原来链表为null,将让当前节点为头结点,让first指向该节点。如果原来的链表不为null,就让pred.next指向newCode
。同时让pred后移。
(4) 现在要做的是把整个链表连接上,分两种情况,如果要插入的节点位于链表的尾部,那么succ=null
,此时让last指向pred
即可。如果不位于尾部,需要让pred.next = succ,succ.prev = pred
/**
* Appends the specified element to the end of this list.
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
//定义一个节点变量l指向last,尾节点不能动,因此定义一个新的节点变量l指向它
final Node<E> l = last;
//定义一个节点newNode,他的prev指向l、他的next指向null
final Node<E> newNode = new Node<>(l, e, null);
//让指向链表尾部的变量last指向newNode
last = newNode;
//如果l为null,说明原来的链表为null,让指向链表头部的指针first指向newNode,让newCode作为头结点
//此时头节点和尾节点都是newCode
if (l == null)
first = newNode;
//否则,让l指针后移,指向新的节点newNode
else
l.next = newNode;
size++;
modCount++;
}
总结:在链表的尾部添加一个元素
(1) 因为尾节点不能动,定义一个节点变量l
指向尾节点
(2) 将添加的元素创建为节点对象newCode
,该节点的prev指向l
,该节点的next指向null
,同时让last指向新添加的节点,如果链表为null,就让该节点作为头结点,同时让first也指向该节点。否则让l.next=newCode
由此可见,链表在节点尾部添加节点效率是非常高的,因此只需要找到last指向的尾部节点,让后将待添加的节点放在该节点后面即可。
/**
* 在指定索引处添加元素
*
* @param index 元素要插入位置的索引
* @param element 要插入的元素
*/
public void add(int index, E element) {
//检查index是否越界(0 <= index <= size)
checkPositionIndex(index);
//如果要插入的位置为链表的尾部
if (index == size)
//否则,如果要插入的位置为链表的中间或头部
else
linkBefore(element, node(index));
}
/**
* 返回在指定索引index处的非空节点
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index < 链表长度的一半,就从头部开始遍历找到index指向的节点
if (index < (size >> 1)) {
//因为头结点不能移动,因此定义一个节点x来遍历链表
//让x指向头结点
Node<E> x = first;
//遍历节点,找到index位置处的节点
for (int i = 0; i < index; i++)
x = x.next;
return x;
//如果index > 链表长度的一半,就从尾部开始遍历找到index指向的节点
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* Inserts element e before non-null Node succ.
*
* @param e 要插入的元素
* @param succ 元素想要插入位置处指向的节点
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//找到要插入位置处的节点的前驱节点
final Node<E> pred = succ.prev;
//创建一个节点对象,让该节点的prev指向pred,该节点的next指向succ
final Node<E> newNode = new Node<>(pred, e, succ);
//让succ的prev重新指向newCode
succ.prev = newNode;
//如果pred==null,说明要插入的位置为链表的头部
if (pred == null)
first = newNode;
//否则让pred后移指向新节点newCode
else
pred.next = newNode;
size++;
modCount++;
}
总结:
(1) 如果待插入的节点位于链表的尾部,直接调用linkLast(element)
方法。
(2) 如果待插入的节点位于链表的头部或中间(此时succ一定不为null,如果为null就说明是链表的尾部),调用linkBefore(E e, Node succ)
方法。这个方法传入两个变量:e
代表待插入的节点元素,succ
代表待插入的位置当前指向的节点。
首先找到要插入位置处节点succ的前驱节点pred,然后创建一个节点对象newCode,让该节点的prev=pred,next=succ
同时让succ.prev = newCode
;
如果pred=null,说明要插入的位置为链表的头部,就让当前节点作为头结点,让first=newCode
,否则pred.next = newNode;
对于链表添加元素的效率比较高,因为只需要遍历链表找到待插入节点的位置,然后将节点插入到该位置即可,node(index)
方法的作用就是查找指定位置处的节点,在查找的过程中,并不是从头向后直接查找指定位置处的节点,如果index<链表长度的一半,就从头节点开始查,如果index>=链表的长度的一半就从尾节点开始查。LinkedList是双向链表。
//判断链表中是否包含某个元素o
public boolean contains(Object o) {
return indexOf(o) != -1;
}
/**
* Returns the index of the first occurrence of the specified element in this list
* 返回在链表中第一次出现这个元素的位置
* @param o element to search for
* @return the index of the first occurrence of the specified element in
* this list, or -1 if this list does not contain the element
*/
public int indexOf(Object o) {
int index = 0;
//因为链表中的元素可以存放null值,因此要分两种情况来讨论
//如果该元素为null,遍历该链表
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
//找到第一个为null的元素
if (x.item == null)
//找到了就返回该元素对应的索引index
return index;
index++;
}
//如果该元素不为null,遍历该链表
} else {
for (Node<E> x = first; x != null; x = x.next) {
//找到第一个该元素
if (o.equals(x.item))
//找到了就返回该元素对应的索引index
return index;
index++;
}
}
//如果没有找到该元素,就返回-1
return -1;
}
总结:
因为链表中可以存放null值,因此判断链表中是否包含某个元素时需要分为两种情况,待查找的元素是否为null。返回的是链表中第一次出现该元素的位置,没有找到就返回-1。
public boolean remove(Object o) {
//同样因为链表中可存放null值,因此需要分两种情况
//如果要删除的元素位null,遍历链表
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
//找到要删除的元素
if (x.item == null) {
//删除该元素,并返回true
unlink(x);
return true;
}
}
//如果要删除的元素不为null,遍历链表
} else {
for (Node<E> x = first; x != null; x = x.next) {
//找到要删除的元素
if (o.equals(x.item)) {
//删除该元素,并返回true
unlink(x);
return true;
}
}
}
//否则返回false
return false;
}
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
//要删除的节点元素
final E element = x.item;
//定义一个变量next指向要删除的节点的后继节点
final Node<E> next = x.next;
//定义一个变量prev要删除的节点的前驱节点
final Node<E> prev = x.prev;
//1、让指向前驱节点的那根线断掉
//如前驱节点为null,说明要删除的节点为头节点,把待删除节点的后一个节点设置为头结点
if (prev == null) {
first = next;
//否则,说明为链表中的某个节点
} else {
//让prev.next指向next
prev.next = next;
//让x.prev置为null
x.prev = null;
}
//2、让指向后继节点的那根线断掉
//如果后继节点为null,说明要删除的节点为尾节点,把待删除节点的前一个节点设置为尾节点
if (next == null) {
last = prev;
//否则
} else {
//让next.prev指向prev
next.prev = prev;
//让x.next指向null
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
从链表中删除指定的元素,要分为两种情况,就是要删除的元素为null值,以及要删除的元素不为null值。原理上都是通过遍历链表找到待删除的元素,然后调用unlink(Node x)
方法删除该元素:
删除元素的时候目的是:将该节点与他的前驱节点连接的两根线断掉,将该节点与他的后继节点连接的两根线断掉
(1)将该节点与他的前驱节点连接的两根线断掉:
如果待删除的节点为头节点,让first=next
;否则,让prev.next = next,x.prev = null
(2) 将该节点与他的后继节点连接的两根线断掉:
如果待删除的节点为尾节点,让last=prev
;否则,让next.prev = prev;x.next = null;
public E remove(int index) {
//判断index索引是否越界
checkElementIndex(index);
return unlink(node(index));
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* 返回在指定索引处的非空节点
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index < 链表长度的一半,就从头部开始遍历找到index指向的节点
if (index < (size >> 1)) {
//因为头结点不能移动,因此定义一个节点x来遍历链表
//让x指向头结点
Node<E> x = first;
//遍历节点,找到index位置处的节点
for (int i = 0; i < index; i++)
x = x.next;
return x;
//如果index > 链表长度的一半,就从尾部开始遍历找到index指向的节点
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
实现队列:
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E element() {
return getFirst();
}
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E remove() {
return removeFirst();
}
public boolean offer(E e) {
return add(e);
}
// Deque operations
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
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 void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}