一 LinkedList
LinkedList 是一个双向链表结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环),在任意位置插入删除都很方便,但是不支持随机取值,每次都只能从一端开始遍历,直到找到查询的对象,然后返回;不过,它不像 ArrayList 那样需要进行内存拷贝,因此相对来说效率较高,但是因为存在额外的前驱和后继节点指针,因此占用的内存比 ArrayList 多一些。
LinkedList
采用链表存储,所以对于add(E e)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i
插入和删除元素的话((add(int index, Eelement)
) 时间复杂度近似为o(n))
因为需要先移动到指定位置再插入
二 LinkedList类源码注释
主要说明是双向链表实现;可以为null值;线程不安全
三 LinkedList类定义
通过类定义可以看到,继承了AbstractSequentialList,实现了List接口,所以它是一个链表,支持相关的添加、删除、修改、遍历等功能,可以看作一个顺序容器;实现了Deque可以看作一个队列(Queue),同时又可以看作一个栈(Stack);实现Cloneable表明是可以被克隆的,自己实现了clone方法;实现Serializable表明支持序列化
四 LinkedList常量定义及内部类Node
// 元素数量
transient int size = 0;
// 头接口
transient Node<E> first;
// 尾节点
transient Node<E> last;
// 内部类Node
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;
}
}
五 构造方法
// 无参构造
public LinkedList() {
}
/**
* 通过一个集合初始化LinkedList,元素顺序有这个集合的迭代器返回顺序决定
*
* @param c 其元素将被放入此列表中的集合
* @throws NullPointerException 如果指定的集合是空的
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
六 新增方法
add一个元素方法和addLast方法是一样的都调用了linkLast方法;新建一个节点,把之前的尾指针节点指向新节点,如果尾节点为空则代表是新链表,直接赋值给头节点
//将指定的元素追加到此列表的末尾。此方法等效于addLast
public boolean add(E e) {
linkLast(e);
return true;
}
// 将e链接为最后一个元素
void linkLast(E e) {
// 获取last元素
final Node<E> l = last;
// 新建一个节点,指定前置节点为刚刚的尾节点,指定当前节点值,后置节点为null
final Node<E> newNode = new Node<>(l, e, null);
// last指向新的节点
last = newNode;
// 判断之前最后一个节点是否空元素,
// 如果为空则表明是一个新的链表插入头,
// 否则把之前的节点尾指针指向新节点
if (l == null)
first = newNode;
else
l.next = newNode;
// 长度++
size++;
modCount++;
}
add一个元素到指定位置;验证越界;判断是否为尾部插入;指定位置插入,把指针移动
// 将指定的元素插入此列表中的指定位置
// 将当前在该位置的元素(如果有)和任何后续元素右移(将其索引添加一个)
public void add(int index, E element) {
// 验证指定长度是否越界,直接判断是否>=0 && <=size
checkPositionIndex(index);
// 判断是否等于链表长度,等于则是尾部新增元素
if (index == size)
linkLast(element);
// 否则是在指定位置新增元素
else
// 此方法和linkLast方法相似,只是指定了一个后置节点
linkBefore(element, node(index));
}
addAll从指定位置新增一个集合;验证越界;转换成数组;获取到前置和后驱节点;遍历成一个新的链表;把新链表加入链表中
addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关,时间复杂度是线性增长
// 从尾部新增一个集合
public boolean addAll(Collection<? extends E> c) {
// 调用从指定位置新增集合
return addAll(size, c);
}
// 从指定位置开始,将指定集合中的所有元素插入此列表
// 将当前在该位置的元素(如果有)和任何后续元素右移(增加其索引)
// 新元素将按指定集合的迭代器返回的顺序显示在列表中
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;
// 如果插入位置为尾部,前驱节点为last,后继节点为null
if (index == size) {
succ = null;
pred = last;
// 否则,调用node()方法得到后继节点,再得到前驱节点
} 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;
}
// 如果插入位置在尾部,重置last节点
if (succ == null) {
last = pred;
// 将插入的链表与先前链表连接起来
} else {
pred.next = succ;
succ.prev = pred;
}
// 长度增加
size += numNew;
modCount++;
return true;
}
七 删除方法
以下方法都是删除头节点;内部核心是unlinkFirst私有方法;删除头节点的值及后驱指针,相当于整个元素为空,等待gc回收;把后置节点移动为头节点,把前置指针置空
删除尾节点原理和删除头节点相似
public E remove() {
return removeFirst();
}
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(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 = next;
// 判断后驱节点是否为空,为空则把尾节点置空
if (next == null)
last = null;
// 否则下一节点前置节点置空(因为现在是头节点)
else
next.prev = null;
// 长度--
size--;
modCount++;
return element;
}
指定位置删除元素,原理是判断是否删除的是头节点或尾节点,如果是则把相应指针移动一个就可以完成,当前元素置空;如果不是则需要把当前节点的前节点的后指针指向当前节点的后节点,把当前节点的后节点的前指针指向当前节点的前节点;相当于把当前节点移除链表,把前节点和后节点相连
// 删除指定位置元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
// 删除指定元素
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;
}
}
// 和if代码块一样只是防止空元素出现空指针
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
// 删除指定元素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;
// 前置节点为空,则当前节点的后驱节点移动到头节点
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;
}
八 查找方法
根据下标获取元素遍历找到当前元素并返回,遍历利用的是判断当前获取元素位于链表的前半段还是后半段,前半段则从头遍历到当前位置返回,后半段则从尾遍历到当前位置返回
getFirst(),element(),peek(),peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中getFirst() 和element() 方法将会在链表为空时,抛出异常;因为内部都保存了头节点所以直接获取头节点就可以
getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只是会返回 null;内部保存了尾节点直接返回即可
// 根据下标获取数据
public E get(int index) {
// 验证越界
checkElementIndex(index);
// 获取node返回当前值
return node(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;
}
}
九 修改方法
通过修改下标获取到下标节点,获取出旧值返回,把新值赋值元素
// 指定位置修改元素
public E set(int index, E element) {
// 验证越界
checkElementIndex(index);
// 获取到要修改下标元素
Node<E> x = node(index);
// 获取到旧值
E oldVal = x.item;
// 修改成新值
x.item = element;
// 返回旧值
return oldVal;
}
十 双向链表与双向循环链表
双向链表就是一个元素有3个属性,一个向前的指针,一个向后的指针,一个当前节点值;双向就是本节点既有向后的指向,也有向前的
双向循环链表的差别在于循环,双向链表首位不相连,指针都指向空,双向循环链表是首位相连形成环状
十一 JDK为什么1.7把双向循环列表改为双向链表
-
双向链表在概念和代码上更清晰
-
双向循环链表是通过new一个headerEntry管理首尾相连得,可以少创建对象
-
写操作主要分为2种,一种头尾插入,一种中间插入;双向链表的有点在于头尾插入的时候只需要维护一个指针,中间插入2个没什么区别,但实际使用中头尾插入是最频繁的