目录
LinkedList概述
简介
我们知道 ArrayList 是基于数组实现的,对于查找的效率极高,但是对于删除效率(涉及到元素的移动)就比较低了。而 LikedList 是基于双向链表实现的,所以和ArrayList 正好相反,对于查找效率相对较低,但是删除效率比 ArrayList 快了很多(不涉及到元素的移动)。LinkedList是非同步的。
类图
图片出处:https://www.cnblogs.com/skywang12345/p/3308807.html
LinkedList数据结构
LinkedList底层数据结构是 双向链表 实现。双向链表就是由一个一个节点(node)通过指针(引用)连接起来的一种链式结构。如下图
图中每一个节点都包含 prev、data、next 三部分,通过prev 、next 和其它节点关联起来,组成了一种链式的结构。注意,双向链表还会包含一个头指针和一个尾指针,目的在于方便遍历链表中的数据,如想要在头部插入一条记录就可以以头指针为参考。
LinkedList核心源码解读(JDK1.8)
重要内部类
//存储元素的节点
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;
}
}
重要成员变量
//元素个数
transient int size = 0;
//指向链表的第一个节点
transient Node<E> first;
//指向链表的最后一个节点
transient Node<E> last;
构造方法
//创建一个空的LinkedList
public LinkedList() {
}
//创建一个包含指定集合 c 中数据的 LinkedList,且元素顺序与 c 迭代的顺序一致
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
添加元素
//在头部添加一个元素(头插法)
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
//保存之前的节点
final Node<E> f = first;
//创建一个新节点,且 next 指向旧头结点
final Node<E> newNode = new Node<>(null, e, f);
//头指针前移(使新创建的节点成为头结点)
first = newNode;
if (f == null)
//当前只有一个节点,该节点既是头结点,也是尾节点
last = newNode;
else
//旧头结点的 prev 指针指向新节点
f.prev = newNode;
size++;
modCount++;
}
//添加一个尾节点(尾插法)
public void addLast(E e) {
linkLast(e);
}
void linkLast(E e) {
//保存旧尾节点
final Node<E> l = last;
//创建新节点,且 pre 指向就尾节点
final Node<E> newNode = new Node<>(l, e, null);
//尾指针后移(使新创建的节点成为新的尾节点)
last = newNode;
if (l == null)
//当前只有一个节点,该节点既是头结点,也是尾节点
first = newNode;
else
//旧尾节点 next 指向新尾节点
l.next = newNode;
size++;
modCount++;
}
删除元素
//删除指定元素 o
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;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
//遍历找到待删除的节点
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//删除指定的节点 x
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
//保存 x 的 next 节点
final Node<E> next = x.next;
//保存 x 的 prev 节点
final Node<E> prev = x.prev;
if (prev == null) {
//如果 pre 节点为空,表示 x 是头结点,所有头指针后移
first = next;
} else {
// x 节点的 pre 节点指向 x 节点的 next 节点(相当于绕过 x 节点)
prev.next = next;
x.prev = null;
}
if (next == null) {
//如果 next 节点为空,表示 x 是尾结点,所以尾指针前移
last = prev;
} else {
// x 节点的 next 节点指向 x 节点的 prev 节点(相当于绕过 x 节点)
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
修改元素
//修改指定 index 处的元素
public E set(int index, E element) {
// 检查是否满足 (0 <= index < size)
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//获取指定 index 处的节点
Node<E> node(int index) {
// assert isElementIndex(index);
// 若 index 是小于 size/2,则从头节点开始找;反之,则从尾节点开始找
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;
}
}
查找
//获取指定 index 处额元素
public E get(int index) {
checkElementIndex(index);
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;
}
}
这里查找操作用了一小技巧,先判断 index 是否大与 size/2,再以此来决定是从头还是从尾开始查找。
其他常用方法
//获取头节点,但不删除
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取头结点,且要删除
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//获取节点,且要删除
public E pop() {
return removeFirst();
}
//插入一个头结点
public void push(E e) {
addFirst(e);
}
//返回true:成功移除指定元素
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
由于 jdk 版本迭代的原因,导致 LinkedList 里面有比较多功能相同的方法,不要太在意,选一个名字更漂亮的用就 ok 了。
遍历方式
通过 Iterator 逐个遍历
Iterator<Integer> l = linkList.iterator();
while(l.hasNext()){
t=l.next();
}
通过 for 循环遍历
for (int i = 0; i < num; i++) {
t = linkList.get(i);
}
通过增强 for 循环遍历
for (Integer tem : linkList) {
t = tem;
}
耗时
第一轮:iterator consume: 5,for consume: 11904,增强for consume: 3
第二轮:iterator consume: 10,for consume: 11089,增强for consume: 4
第三轮:iterator consume: 6,for consume: 12094,增强for consume: 33
从上面的结果可知,iterator方式和挣钱for的方式平均下来效率是差不多的,所以选哪一种都可行。但是 for 循环是一如既往的慢,所以遍历LinkedList,一定不要用 for 循环的方式
测试代码
public class LinkedListTest {
public static void main(String[] args) {
int t = 0;
LinkedList<Integer> linkList = new LinkedList<Integer>();
int num = 100000;
for (int i = 0; i < num; i++) {
linkList.add(i);
}
long startTime =0;
startTime = System.currentTimeMillis();
Iterator<Integer> l = linkList.iterator();
while(l.hasNext()){
t=l.next();
}
System.out.println("iterator consume: " + (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
t = linkList.get(i);
}
System.out.println("for consume: " + (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
for (Integer tem : linkList) {
t = tem;
}
System.out.println("增强for consume: " + (System.currentTimeMillis() - startTime));
}
}
总结
LinkedList 具备以下特点:删除效率高(不涉及元素移动)、查找效率低(每次都需要遍历链表)、不是线程安全的、没有固定大小的容量。如果需要保证线程安全,可以通过下面实现
List list = Collections.synchronizedList(new LinkedList(...));
Thanks: