技术小菜比入坑 LinkedList,i 了 i 了

先看再点赞,给本身一点思考的时间,微信搜索【沉默王二】关注这个靠才华苟且的程序员。
本文 GitHub github.com/itwanger 已收录,里面还有一线大厂整理的面试题,以及个人系列文章。java

上一篇入坑了 ArrayList,小伙伴们反响不错,那这篇就继续入坑 LinkedList,它俩算是亲密无间的兄弟,相爱相杀的那种,不离不弃的那种,介绍了这个就必须介绍那个的那种。node

明目张胆地告诉你们一个好消息,我写了一份 4 万多字的 Java 小白手册,小伙伴们能够在「沉默王二」公众号后台回复「小白」获取免费下载连接。以为不错的话,请随手转发给身边须要的小伙伴,赠人玫瑰,手有余香哈。git

最开始学习 Java 的时候,我还挺纳闷的,有了 ArrayList,干吗还要 LinkedList 啊,都是 List,不是不少余吗?当时真的很傻很天真,不知道有没有同款小伙伴。搞不懂二者之间的区别,什么场景下该用 ArrayList,什么场景下该用 LinkedList,傻傻分不清楚。那么这篇文章,能够一脚把这种天真踹走了。程序员

和数组同样,LinkedList 也是一种线性数据结构,但它不像数组同样在连续的位置上存储元素,而是经过引用相互连接。github

LinkedList 中的每个元素均可以称之为节点(Node),每个节点都包含三个项目:其一是元素自己,其二是指向下一个元素的引用地址,其三是指向上一个元素的引用地址。web

Node 是 LinkedList 类的一个私有的静态内部类,其源码以下所示:面试

private static class Node<E{
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;

    Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
复制代码

LinkedList 看起来就像下面这个样子:算法

  • 第一个节点因为没有前一个节点,因此 prev 为 null;数组

  • 最后一个节点因为没有后一个节点,因此 next 为 null;微信

  • 这是一个双向链表,每个节点都由三部分组成,先后节点和值。

那可能有些小伙伴就会和当初的我同样,好奇地问,“为何要设计 LinkedList 呢?”若是能给 LinkedList 类的做者打个电话就行了,惋惜没有他的联系方式。很遗憾,只能靠我来给你们解释一下了。

第一,数组的大小是固定的,即使是 ArrayList 能够自动扩容,但依然会有必定的限制:若是声明的大小不足,则须要扩容;若是声明的大小远远超出了实际的元素个数,又会形成内存的浪费。尽管扩容的算法已经很是优雅,尽管内存已经绰绰有余。

第二,数组的元素须要连续的内存位置来存储其值。这就是 ArrayList 进行删除或者插入元素的时候成本很高的真正缘由,由于咱们必须移动某些元素为新的元素留出空间,好比说:

如今有一个数组,十、十二、1五、20、四、五、100,若是须要在 12 的位置上插入一个值为 99 的元素,就必须得把 12 之后的元素日后移动,为 99 这个元素腾出位置。

删除是一样的道理,删除以后的全部元素都必须往前移动一次。

LinkedList 就摆脱了这种限制:

第一,LinkedList 容许内存进行动态分配,这就意味着内存分配是由编译器在运行时完成的,咱们无需在 LinkedList 声明的时候指定大小。

第二,LinkedList 不须要在连续的位置上存储元素,由于节点能够经过引用指定下一个节点或者前一个节点。也就是说,LinkedList 在插入和删除元素的时候代价很低,由于不须要移动其余元素,只须要更新前一个节点和后一个节点的引用地址便可。

LinkedList 类的层次结构以下图所示:

  • LinkedList 是一个继承自 AbstractSequentialList 的双向链表,所以它也能够被看成堆栈、队列或双端队列进行操做。

  • LinkedList 实现了 List 接口,因此能对它进行队列操做。

  • LinkedList 实现了 Deque 接口,因此能将 LinkedList 看成双端队列使用。

明白了 LinkedList 的一些理论知识后,咱们来看一下如何使用 LinkedList。

0一、如何建立一个 LinkedList

LinkedList<String> list = new LinkedList<>();
复制代码

和建立 ArrayList 同样,能够经过上面的语句来建立一个字符串类型的 LinkedList(经过尖括号来限定 LinkedList 中元素的类型,若是尝试添加其余类型的元素,将会产生编译错误)。

不过,LinkedList 没法在建立的时候像 ArrayList 那样指定大小。

0二、向 LinkedList 中添加一个元素

能够经过 add() 方法向 LinkedList 中添加一个元素:

LinkedList<String> list = new LinkedList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");
复制代码

感兴趣的小伙伴能够研究一下 add() 方法的源码,它在添加元素的时候会调用 linkLast() 方法。

void linkLast(E e) {
    final LinkedList.Node<E> l = last;
    final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
复制代码

添加第一个元素的时候,last 为 null,建立新的节点(next 和 prev 都为 null),而后再把新的节点赋值给 last 和 first;当添加第二个元素的时候,last 为第一个节点,建立新的节点(next 为 null,prev 为第一个节点),而后把 last 更新为新的节点,first 保持不变,第一个节点的 next 更新为第二个节点;以此类推。

还能够经过 addFirst() 方法将元素添加到第一位;addLast() 方法将元素添加到末尾;add(int index, E element) 方法将元素添加到指定的位置。

0三、更新 LinkedList 中的元素

可使用 set() 方法来更改 LinkedList 中的元素,须要提供下标和新元素。

list.set(0"沉默王五");
复制代码

来看一下 set() 方法的源码:

public E set(int index, E element) {
    checkElementIndex(index);
    LinkedList.Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
复制代码

该方法会先对指定的下标进行检查,看是否越界,而后根据下标查找节点:

LinkedList.Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        LinkedList.Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        LinkedList.Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
复制代码

node() 方法会对下标进行一个初步的判断,若是靠近末端,则从最后开始遍历,这样可以节省很多遍历的时间,小伙伴们眼睛要睁大点了,这点要学。

找到节点后,再替换新值并返回旧值。

0四、删除 LinkedList 中的元素

能够经过 remove() 方法删除指定位置上的元素:

 list.remove(1);
复制代码

该方法会调用 unlink() 方法对先后节点进行更新。

unlink(LinkedList.Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final LinkedList.Node<E> next = x.next;
    final LinkedList.Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}
复制代码

还可使用 removeFirst()removeLast() 方法删除第一个节点和最后一个节点。

0五、查找 LinkedList 中的元素

若是要正序查找一个元素,可使用 indexOf() 方法;若是要倒序查找一个元素,可使用 lastIndexOf() 方法。

来看一下 indexOf() 方法的源码:

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}
复制代码

基本上和 ArrayList 的大差不差,都须要遍历,若是要查找的元素为 null,则使用“==”操做符,能够避免抛出空指针异常;不然使用 equals() 方法进行比较。

另外,getFirst() 方法用于获取第一个元素;getLast() 方法用于获取最后一个元素;poll()pollFirst() 方法用于删除并返回第一个元素(两个方法尽管名字不一样,但方法体是彻底相同的);pollLast() 方法用于删除并返回最后一个元素;peekFirst() 方法用于返回但不删除第一个元素。

0六、最后

若是要咱们本身实现一个链表的话,上面这些增删改查的轮子方法是必定要白嫖啊,不对,必定要借鉴啊。

上一篇 ArrayList 中提到过,随机访问一个元素的时间复杂度为 O(1),但 LinkedList 要复杂一些,由于数据增大多少倍,耗时就增大多少倍,由于要循环遍历,因此时间复杂度为 O(n)。

至于 LinkedList 在插入、添加、删除元素的时候有没有比 ArrayList 更快,这要取决于数据量的大小,以及元素所在的位置。不过,从理论上来讲,因为不须要移动数组,应该会更快一些。但到底快不快,下一篇带来答案,小伙伴们敬请期待。


我是沉默王二,一枚有颜值却靠才华苟且的程序员。关注便可提高学习效率,别忘了三连啊,点赞、收藏、留言,我不挑,奥利给

注:若是文章有任何问题,欢迎绝不留情地指正。

若是你以为文章对你有些帮助欢迎微信搜索「沉默王二」第一时间阅读,回复「小白」更有我肝了 4 万+字的 Java 小白手册 2.0 版,本文 GitHub github.com/itwanger 已收录,欢迎 star。

相关文章
相关标签/搜索