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

慢得
1 声望0 粉丝

目前就职于国内支付金融独角兽公司,专注于后端开发知识和经验分享,交流沟通+V,ly853602