1 核心知识点
- 数据结构
- 链表结构分析
- 关键设计
- 线程安全
2 关键代码分析
- 存储结构
LinkedList,首先说说它的链表数据结构,清楚链表的特性,可以帮助更好的匹配LinkedList的使用场景。
LinkedList是一个链表结构,链表中的节点就是存储元素的地方,看看它的定义
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;
}
}
从上面代码,可以清晰的看到,每一个节点是由三个部分组成,item,next,prev,其中item就是我们存储的数据,next和prev分别是指针,既然有向前和向后的指针,说明这个链表是一个双向链表,可以根据当前节点,往前或者往后进行寻找下一个元素。
2.1 add方法
public boolean add(E e) {
// 根据字面意思,就是在链表尾部添加元素
linkLast(e);
return true;
}
void linkLast(E e) {
// 先将之前的尾部元素赋值给l
final Node<E> l = last;
// 然后通过构造器新增一个节点,构造其中的第一个元素是prev,也就是
// 指向的上一个节点,e是item,当前节点元素数据,null是next
// 下一个节点,因为这个节点是最新的尾部节点,所以,下一个节点指向
// 为null
final Node<E> newNode = new Node<>(l, e, null);
// 将新的节点赋值给last
last = newNode;
// 这里的判断是识别首次add,需要将新的节点赋值给first节点
if (l == null)
first = newNode;
else
// 如果不是首次,那么需要将前一个节点的向后指针,指向新
// 创建的节点
l.next = newNode;
// 容量+1
size++;
modCount++;
}
- 掌握新增节点的核心逻辑,通过Node构造器构建新的Node节点,然后,将其放在链表的尾部
- 前一个节点的指针指向下一个节点
- 添加元素,操作的时间复杂度是O(1)
2.2 remove方法
public boolean remove(Object o) {
// 判断需要移除的元素是否为null
// 之所以要将null单独提出来,是避免NPE
if (o == null) {
//遍历移除为null的节点
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
// 这里可以详细看看,链表是怎么移除节点的
unlink(x);
return true;
}
}
} else {// 移除元素不为null的情况下,遍历删除
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;
// 如果前一个节点为null,那么当前节点就是头节点
// 直接将first指向下一个节点
if (prev == null) {
first = next;
} else {// 否则,将前一个节点的向后指针指向下个节点
// 同时将向前指针置为null
prev.next = next;
x.prev = null;
}
/ // 如果后一个节点是null,那说明当前节点就是尾节点,
// 直接将last指向上一个节点
if (next == null) {
last = prev;
} else {// 否则下一个节点的向前指针指向上一个节点
// 同时将向后指针置为null
next.prev = prev;
x.next = null;
}
/ // 前面已经把当前节点的向前,向后指针置为null
// 最后需要将当前节点的元素item也置为null
x.item = null;
size--;
modCount++;
return element;
}
- 将当前节点的元素数据,向前指针,向后指针置为空
- 将当前节点的前一个节点的向后指针,指向当前节点的下一个节点
- 将当前节点的后一个节点的向前指针,指向当前节点的上一个节点
- 删除的时间复杂度O(1),但是要删除这个元素,需要先找到这个元素,而找到这个元素需要遍历整个链表,时间复杂度是O(n),整个方法的时间复杂度是O(n)
2.3 contains方法
public boolean contains(Object o) {
// 核心逻辑是判断链表中改元素的数量是否>0
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
int index = 0;
// 逻辑比较简单,就是遍历链表,查找元素,存在,则数量+1
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
- 判断链表中该元素是否存在的逻辑比较简单,就是遍历链表判断
- 时间复杂度是O(n)
3 线程安全问题
- 是否线程安全
通过上面代码的分析,可以发现,全程操作是没有做任何线程安全处理的,所以也是线程非安全
- 如何做到线程安全
方法一:使用Collections.synchronizedList进行包装,所有操作都加上锁,缺点是并发低,同一时刻,只能有一个线程操作LinkedList对象。
方法二:使用 JUC 中的 ConcurrentLinkedQueue,专门为高并发场景设计的队列实现类,内部通过一些复杂的无锁算法(基于 CAS 操作等机制)来保证在多线程环境下多个线程对队列进行插入( offer 方法、删除(poll 方法)、获取元素(peek 方法)等操作时的线程安全性,能高效地支持多个线程同时操作队列,而不需要像传统的加锁方式那样带来较大的性能开销,常用于多线程生产者-消费者模式等场景中处理任务队列等情况。
4 时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
添加 | O(1) | |
查找 | O(n) | |
删除 | O(n) | 删除节点包含了查找和删除两个动作<br/>其中删除是O(1),但是查找是O(n) |
5 面试技巧
- 说清数据结构
linkedList底层是一个双向链表结构,链表中的每个节点是一个node,每个node由元素值item,以及向上指针prev和向下指针next构成。
- 说明主要操作的时间复杂度,展现基本功
在这种数据机构中,如果是添加和删除,只需要改变指针的引用即可,时间复杂度为O(1),但是查询需要遍历节点才能找到对应的节点值,但是linkedlist做了优化,会将遍历分成前后两部分,然后判断查询的索引在那一块区域,如果在前半部分,从头结点开始遍历,如果是后半部分,从尾结点开始遍历,总的来说,时间复杂度为O(n)。
- 强调线程安全问题,突出高并发编程能力和经验
欢迎交流沟通,可+V:ly853602
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。