双向链表
数据结构。链表是由一系列 节点(Node)
组成的,每个节点包含了指向 上一个节点的指针prev
, 数据item
和 指向下一个节点next的指针
。
Deque
接口,可以在两端
进行操作(插入、删除)。并且由于LinkedList内部是基于链表实现的,所以插入、删除数据时只需要改变链表指针的指向
,时间复杂度为O(1)
,而不需要进行数组的移动,所以它非常适合于频繁的插入、删除操作。但是LinkedList的缺点就是随机访问元素的速度较慢,因为需要从头开始遍历链表
,时间复杂度为O(n)
。Cloneable
接口,支持克隆
功能Iterable
接口,可以使用 for-each
迭代List
接口,支持 增删改查
的功能源码分析(JDK1.8)
/**
* 链表节点个数
*/
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;
}
}
LinkedList 有两个构造函数,空参构造方法,指定集合元素列表的构造方法
/**
* 构造一个空列表
*/
public LinkedList() {
}
/**
* 按照集合迭代器返回的顺序,构造一个包含指定集合的元素的列表
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
/**
* 将指定集合中的所有元素从此列表中的指定位置开始插入。将当前位于该位置的元素(如果有)
* 和任何后续元素向右移动(增加它们的索引)。新元素将按照指定集合的迭代器返回的顺序出现
* 在列表中。
*/
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); //校验index是否有效,是否超出链表的实际长度
Object[] a = c.toArray(); //将集合转换成Object[] 数组
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
// 如果索引值与链表的长度一致.那么将此指定集合中的所有元素放在链表的末尾
if (index == size) {
succ = null;
pred = last;
} else {
// 如果不一致,就需要将指定集合中的所有元素从此列表中的指定位置开始插入
succ = node(index); //从链表中获取当前index位置的节点Node
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;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew; // 重新标记链表的长度
modCount++;
return true;
}
ArrayList 的add方法有两个,末尾添加
和指定位置添加
/**
* 将指定的元素追加到此列表的末尾
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* 将e链接为最后一个元素.
*/
void linkLast(E e) {
final Node<E> l = last; //先获取链表最后一个节点
//将l节点设置为e元素节点上一个节点,因为是末尾添加,所以e元素的下一个节点next为null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; //先标记新增加的e元素节点为链表最末尾节点
if (l == null)
//如果l节点为null,说明当前链表没有任何节点,将新增的e元素节点设置为链表头节点
first = newNode;
else
//当前链表有其他节点,将l节点的next下一个节点指定为新增的e元素节点
l.next = newNode;
size++; //更新链表长度+1
modCount++; //记录链表数据结构的变化次数
}
public void add(int index, E element) {
//判断索引下标是否在链表长度范围内,超出范围则报IndexOutOfBoundsException
checkPositionIndex(index);
if (index == size)
//指定位置index正好等于链表的长度,那么就是尾部添加节点
linkLast(element);
else
//复杂一点,见下文源码
linkBefore(element, node(index));
}
/**
* 指定位置index之前添加节点
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; //获取指定位置index节点的上一个节点pred
//将新增e元素节点的上一个节点指定为succ.prev,下一个节点next指向为succ
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode; //变更succ的上一个节点为新增e元素节点
if (pred == null)
//表明当前链表还没任何节点,标记新增e元素节点为链表的头节点
first = newNode;
else
//表明当前链表有节点,
pred.next = newNode,将succ.prev节点的下一个节点next更细为新增e元素节点
size++; //更新链表长度+1
modCount++; //记录链表数据结构的变化次数
}
LinkedList 的remove方法有两个,指定节点删除
和指定位置删除
;
public boolean remove(Object o) {
if (o == null) {
//指定的节点元素item为null,那么就从链表的frist节点开始遍历,将所有节点之间解绑
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;
}
/**
* 解绑
*/
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; //将前一个节点prve置为null
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null; //将下一个节点next置为null
}
x.item = null; //将节点元素item置为null
size--; //链表节点个数 -1
modCount++;
return element;
}
IndexOutOfBoundsException
public E remove(int index) {
checkElementIndex(index); //校验index的有效性
return unlink(node(index)); //先获取index位置的节点,然后解绑
}
/**
* 解绑
*/
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; //将前一个节点prve置为null
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null; //将下一个节点next置为null
}
x.item = null; //将节点元素item置为null
size--; //链表节点个数 -1
modCount++;
return element;
}
LinkedList 中的修改方法只有一个,通过index下标来精准修改, 修改元素节点的步骤,其实就是同一个位置的节点item的覆盖操作,set方法只修改节点的item信息,prev和next信息不变
public E set(int index, E element) {
checkElementIndex(index); //校验index的有效性,IndexOutOfBoundsException
Node<E> x = node(index); //先获取index位置的节点
E oldVal = x.item;
x.item = element; //重新赋值
return oldVal;
}
LinkedList 的get方法只有一个,只能通过索引下标index获取;先取链表的长度size/2
值,然后与index做比较,如果index < size /2 ,那么就从左向右开始遍历; 如果index > size /2 ,那么就从右向左开始遍历;这样相比全节点从frist 向 last遍历,可节省一半的遍历时间
public E get(int index) {
checkElementIndex(index); //校验index的有效性,IndexOutOfBoundsException
return node(index).item; //核心, 有算法
}
/**
* 校验遍历的方向
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //用index与链表长度size的一半比较,右移>>位运算
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next; //从first开始,从前往后一个个遍历直到到达index所在位置
return x;
} else {
Node<E> x = last; //从last开始,从后向前一个个遍历直到到达index所在位置
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
ArrayList和LinkedList都是实现了List接口的数据结构,但它们在内部实现和使用方面有所不同:
区别 | ArrayList | LinkedList |
---|---|---|
内部实现 | 动态数组 | 双向链表 |
访问方式 | 索引访问,时间复杂度O(1) | 从头部或尾部开始遍历链表来访问元素,时间复杂度O(n) |
添加/删除元素 | 需要进行数组的扩容 或数据元素的移动,时间复杂度O(n) |
只需调整指针的指向,时间复杂度O(1) |
遍历效率 | 遍历效率比LinkedList高,因为ArrayList的元素在内存中连续存储,可以利用CPU缓存机制进行快速遍历 | 元素在内存中是分散存储的,不能利用CPU缓存机制进行快速遍历 |
内存空间 | 空间浪费比LinkedList少 | 每个节点都需要额外的空间来存储指针 |