java容器类:LinkedList

ArrayList随机访问效率很高,但插入和删除性能比较低,我们提到了同样实现了List接口的LinkedList,它的特点与ArrayList几乎正好相反,本节我们就来详细介绍LinkedList。

实现原理

内部组成

我们知道,ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList直译就是链表,确切的说,它的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起,类似于小朋友之间手拉手一样。

为了表示链接关系,需要一个节点的概念,节点包括实际的元素,但同时有两个链接,分别指向前一个节点(前驱)和后一个节点(后继),节点是一个内部类,具体定义为:

1
2
3
4
5
6
7
8
9
10
11
private static class Node {
E item;
Node next;
Node prev;

Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

Node类表示节点,item指向实际的元素,next指向下一个节点,prev指向前一个节点。

LinkedList内部组成就是如下三个实例变量:

1
2
3
transient int size = 0;
transient Node first;
transient Node last;

我们暂时忽略transient关键字,size表示链表长度,默认为0,first指向头节点,last指向尾节点,初始值都为null。

LinkedList的所有public方法内部操作的都是这三个实例变量,具体是怎么操作的?链接关系是如何维护的?我们看一些主要的方法,先来看add方法。

Add方法

add方法的代码为:

1
2
3
4
public boolean add(E e) {
linkLast(e);
return true;
}

主要就是调用了linkLast,它的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void linkLast(E e) {
final Node l = last;
//创建一个新的节点newNode。prev指向原来的尾节点,如果原来链表为空,则为null
final Node newNode = new Node<>(l, e, null);
//修改尾节点last,指向新的最后节点newNode
last = newNode;
//修改前节点的后向链接,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点。
if (l == null)
first = newNode;
else
l.next = newNode;
//增加链表大小
size++;
//修改次数增加,便于迭代期间检测结构变化
modCount++;
}

可以看出,与ArrayList不同,LinkedList的内存是按需分配的,不需要预先分配多余的内存,添加元素只需分配新元素的空间,然后调节几个链接即可。

根据索引访问元素 get

添加了元素,如果根据索引访问元素呢?我们看下get方法的代码:

1
2
3
4
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

checkElementIndex检查索引位置的有效性,如果无效,抛出异常,代码为:

1
2
3
4
5
6
7
8
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}

如果index有效,则调用node方法查找对应的节点,其item属性就指向实际元素内容,node方法的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
Node node(int index) {
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

size>>1等于size/2,如果索引位置在前半部分 (index<(size>>1)),则从头节点开始查找,否则,从尾节点开始查找。

可以看出,与ArrayList明显不同,ArrayList中数组元素连续存放,可以直接随机访问,而在LinkedList中,则必须从头或尾,顺着链接查找,效率比较低。

插入元素

add是在尾部添加元素,如果在头部或中间插入元素呢?可以使用如下方法:

1
public void add(int index, E element)

它的代码是:

1
2
3
4
5
6
7
8
public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

如果index为size,添加到最后面,一般情况,是插入到index对应节点的前面,调用方法为linkBefore,它的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void linkBefore(E e, Node succ) {
final Node pred = succ.prev;
//新建一个节点newNode,前驱为pred,后继为succ
final Node newNode = new Node<>(pred, e, succ);
//让后继的前驱指向新节点
succ.prev = newNode;
//让前驱的后继指向新节点,如果前驱为空,修改头节点指向新节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

删除元素

删除x节点,基本思路就是让x的前驱和后继直接链接起来,next是x的后继,prev是x的前驱,具体分为两步:

  1. 第一步是让x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
  2. 第二步是让x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。

LinkedList特点分析

LinkedList内部是用双向链表实现的,维护了长度、头节点和尾节点,这决定了它有如下特点:

  • 按需分配空间,不需要预先分配很多空间
  • 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
  • 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
  • 在两端添加、删除元素的效率很高,为O(1)。
  • 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。

理解了LinkedList和ArrayList的特点,我们就能比较容易的进行选择了,如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,则LinkedList就是比较理想的选择。


相关链接:https://juejin.im/post/58204cd2a0bb9f0058bd388f

   

你可能感兴趣的:(java基础)