深入Java集合LinkedList

现在由大恶人付有杰来从增删改查几个角度轻度解析LinkedList的源码

1.整体架构

LinkedList 底层数据结构是一个双向链表(),整体结构如下图所示:
在这里插入图片描述
链表中的每个节点都可以向前或者向后追溯,我们有几个概念如下:

  1. 链表每个节点我们叫做 Node,Node 有 prev 属性,代表前一个节点的位置,next 属性,代表后一个节点的位置;
  2. first 是双向链表的头节点,它的前一个节点是 null。
  3. last 是双向链表的尾节点,它的后一个节点是 null;
  4. 当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null;
  5. 因为是个双向链表,只要机器内存足够强大,是没有大小限制的。
    链表中的元素叫做 Node,我们看下 Node 的组成部分:
    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;
    }
}

添加

链表的添加方式有很多,就说最常用的。add

public boolean add(E e) {
        linkLast(e);
        return true;
    }
// 从尾部开始追加节点
void linkLast(E e) {
    // 把尾节点数据暂存
    final Node<E> l = last;
    // 新建新的节点,初始化入参含义:
    // l 是新节点的前一个节点,当前值是尾节点值
    // e 表示当前新增节点,当前新增节点后一个节点是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 新建节点追加到尾部
    last = newNode;
    //如果链表为空(l 是尾节点,尾节点为空,链表即空),头部和尾部是同一个节点,都是新建的节点
    if (l == null)
        first = newNode;
    //否则把前尾节点的下一个节点,指向当前尾节点。
    else
        l.next = newNode;
    //大小和版本更改
    size++;
    modCount++;
}

从源码上来看,尾部追加节点比较简单,只需要简单地把尾巴引用指向位置修改下即可.(会画图的人画图发博客,不会画图的人打字发博客)
当然还有addFirst
此方法用于在头部添加数据:

 public void addFirst(E e) {
        linkFirst(e);
    }
// 从头部追加
private void linkFirst(E e) {
    // 头节点赋值给临时变量
    final Node<E> f = first;
    // 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值
    final Node<E> newNode = new Node<>(null, e, f);
    // 新建节点成为头节点
    first = newNode;
    // 头节点为空,就是链表为空,头尾节点是一个节点
    if (f == null)
        last = newNode;
    //上一个头节点的前一个节点指向当前节点
    else
        f.prev = newNode;
    size++;
    modCount++;
}

头部追加节点和尾部追加节点非常类似,只是前者是移动头节点的 prev 指向,后者是移动尾节点的 next 指向。

节点删除

在看节点删除之前,我问你,你知道了这个双链表点节点,你会如何删除它?
我就画个图给你看看:
假设要删除的是第二个节点
在这里插入图片描述
**第一步:**修改该节点的前驱节点的next指向自己的next的指向

代码表示
node2.pre.next = node2.next;

在这里插入图片描述
第二步修改自己后面的节点的pre指针指向自己的前一个结点

代码表示
node2.next.pre = node2.pre;

在这里插入图片描述
第三步:将自己的pre,next全部清空

代码表示
node2.next = null;
node2.pre = null;

在这里插入图片描述

节点查询

因为链表不可以像数组那么样按照索引访问 元素,链表查询某一个节点是比较慢的,需要挨个循环查找才行,我们看看 LinkedList 的源码是如何寻找节点的:

// 根据链表索引位置查询节点
Node<E> node(int index) {
    // 如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 直到 for 循环到 index 的前一个 node 停止
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {// 如果 index 处于队列的后半部分,从尾开始找
        Node<E> x = last;
        // 直到 for 循环到 index 的后一个 node 停止
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

从源码中我们可以发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能,我只能说,写的不错!!!。

其实链表就将每个节点通过前后指针连接起来,也就没有那么什么扩容的麻烦啦。如果要按照索引的位置添加元素,就必须遍历链表的同时index++,然后确定元素,直接断开连接,新增就可以了,不用移动元素那么麻烦。所以呢链表添加删除的时候还是很简便的。
我画个图吧:
在这里插入图片描述
代码展示就是

//找到符合位置的节点  要添加的节点
add(Node node,Node e){
//获取前面的节点
	Node pre = node.pre;
	//后面的节点
	Node next = node.next;
	e.pre = node.pre;
	//我在在你的位置 上插入,自然我的下一个节点是你
	e.next = node;
	node.pre.next = e;
	node.pre = null;
	node.next = null;
}

迭代器

因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做:ListIterator,这个接口提供了向前和向后的迭代方法,如下所示:
在这里插入图片描述

// 双向迭代器
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;//上一次执行 next() 或者 previos() 方法时的节点位置
    private Node<E> next;//下一个节点
    private int nextIndex;//下一个节点的位置
    //expectedModCount:期望版本号;modCount:目前最新版本号
    private int expectedModCount = modCount;
    …………
}

我们先来看下从头到尾方向的迭代:
// 判断还有没有下一个元素

public boolean hasNext() {
    return nextIndex < size;// 下一个节点的索引小于链表的大小,就有
}

// 取下一个元素
public E next() {
    //检查期望版本号有无发生变化
    checkForComodification();
    if (!hasNext())//再次检查
        throw new NoSuchElementException();
    // next 是当前节点,在上一次执行 next() 方法时被赋值的。
    // 第一次执行时,是在初始化迭代器的时候,next 被赋值的
    lastReturned = next;
    // next 是下一个节点了,为下次迭代做准备
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
// 如果上次节点索引位置大于 0,就还有节点可以迭代
public boolean hasPrevious() {
    return nextIndex > 0;
}
// 取前一个节点
public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();
    // next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
    // next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
    lastReturned = next = (next == null) ? last : next.prev;
    // 索引位置变化
    nextIndex--;
    return lastReturned.item;
}

链表就比数组简单多了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章