LinkedList概述
LinkedList继承自AbstractSequentialList,实现了List接口和Deque接口。
既可以当做list用,也可作为队列和栈来用。可以说是非常的全面了。
本文分析基于jdk1.8
LinkedList底层实现原理
底层数据结构
LinkedList底层采用双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。
在这里没有所谓的哑元,当链表为空的时候first和last都指向null。
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; |
| } |
| } |
first和last节点:
| /** |
| * Pointer to first node. |
| * Invariant: (first == null && last == null) || |
| * (first.prev == null && first.item != null) |
| */ |
| transient Node<E> first; |
| |
| /** |
| * Pointer to last node. |
| * Invariant: (first == null && last == null) || |
| * (last.next == null && last.item != null) |
| */ |
| transient Node<E> last; |
时间复杂度
LinkedList的实现方式决定了所有跟下表有关的操作都是线性时间,而在首段或者末尾删除元素只需要常数时间。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。
LinkedList的添加操作
add(E e)方法
add()方法有两个版本,一个add(E e),该方法在LinkedList的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间,只需简单修改几个相关引用即可;
另外一个add(int index,E e),该方法是在指定下标出插入元素,需要通过线性查找找到具体位置,然后修改相关引用完成插入操作。
| public boolean add(E e) { |
| linkLast(e); |
| return true; |
| } |
| |
| |
| |
| |
| void linkLast(E e) { |
| final Node<E> l = last; |
| final Node<E> newNode = new Node<>(l, e, null); |
| last = newNode; |
| if (l == null) |
| first = newNode; |
| else |
| l.next = newNode; |
| size++; |
| modCount++; |
| } |
| |
| |
| |
| |
| private void linkFirst(E e) { |
| final Node<E> f = first; |
| final Node<E> newNode = new Node<>(null, e, f); |
| first = newNode; |
| if (f == null) |
| last = newNode; |
| else |
| f.prev = newNode; |
| size++; |
| modCount++; |
| } |
add(int index, E element)方法
| public void add(int index, E element) { |
| checkPositionIndex(index); |
| |
| if (index == size) |
| linkLast(element); |
| else |
| linkBefore(element, node(index)); |
| } |
上面代码中的node(int index)函数有一点小trick,因为链表时双向的,可以从后往前找,也可以从前往后招,具体朝哪个方向取决于条件 index < (size >> 1) ,也就是index是靠近前端还是后端。从这里可以看出,linkedList通过index检查元素的效率没有arrayList高。
addAll(Collection<? extends E> c)方法
addAll(index,c)实现方式并不是直接调用add(index,e)实现的,主要是因为效率问题,另一个就是fail-fast中modCount只会增加1次;
| public boolean addAll(Collection<? extends E> c) { |
| return addAll(size, c); |
| } |
| |
| public boolean addAll(int index, Collection<? extends E> c) { |
| checkPositionIndex(index); |
| |
| Object[] a = c.toArray(); |
| int numNew = a.length; |
| if (numNew == 0) |
| return false; |
| |
| Node<E> pred, succ; |
| if (index == size) { |
| succ = null; |
| pred = last; |
| } else { |
| succ = node(index); |
| pred = succ.prev; |
| } |
| |
| for (Object o : a) { |
| @SuppressWarnings("unchecked") E e = (E) o; |
| Node<E> newNode = new Node<>(pred, e, null); |
| if (pred == null) |
| first = newNode; |
| else |
| pred.next = newNode; |
| pred = newNode; |
| } |
| |
| if (succ == null) { |
| last = pred; |
| } else { |
| pred.next = succ; |
| succ.prev = pred; |
| } |
| |
| size += numNew; |
| modCount++; |
| return true; |
| } |
addFirst(E e)和addLast(E e)方法
| |
| |
| |
| |
| |
| public void addFirst(E e) { |
| linkFirst(e); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| public void addLast(E e) { |
| linkLast(e); |
| } |
基于队列接口的offer方法
| public boolean offer(E e) { |
| return add(e); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public boolean offerFirst(E e) { |
| addFirst(e); |
| return true; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| public boolean offerLast(E e) { |
| addLast(e); |
| return true; |
| } |
基于栈的push方法
| public void push(E e) { |
| addFirst(e); |
| } |
LinkedList的删除操作
remove()和removeFirst()方法
| |
| |
| |
| |
| |
| |
| |
| public E remove() { |
| return removeFirst(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| public E removeFirst() { |
| final Node<E> f = first; |
| if (f == null) |
| throw new NoSuchElementException(); |
| return unlinkFirst(f); |
| } |
| |
| |
| |
| |
| private E unlinkFirst(Node<E> f) { |
| |
| final E element = f.item; |
| final Node<E> next = f.next; |
| f.item = null; |
| f.next = null; |
| first = next; |
| if (next == null) |
| last = null; |
| else |
| next.prev = null; |
| size--; |
| modCount++; |
| return element; |
| } |
基于队列的poll()方法
| public E poll() { |
| final Node<E> f = first; |
| return (f == null) ? null : unlinkFirst(f); |
| } |
基于栈的pop()方法
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public E pop() { |
| return removeFirst(); |
| } |
LinkedList的查找操作
LinkedList 底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(n)。
get(int index)方法
| |
| |
| |
| |
| |
| |
| |
| public E get(int index) { |
| checkElementIndex(index); |
| return node(index).item; |
| } |
| |
| |
| |
| |
| Node<E> node(int index) { |
| |
| |
| if (index < (size >> 1)) { |
| Node<E> x = first; |
| for (int i = 0; i < index; i++) |
| x = x.next; |
| return x; |
| } else { |
| Node<E> x = last; |
| for (int i = size - 1; i > index; i--) |
| x = x.prev; |
| return x; |
| } |
| } |
| |
| private void checkElementIndex(int index) { |
| if (!isElementIndex(index)) |
| throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); |
| } |
| |
| private boolean isElementIndex(int index) { |
| return index >= 0 && index < size; |
| } |
主要是通过遍历的方式定位目标位置的节点。获取到节点后,取出节点存储的值返回即可。这里面有个小优化,即通过比较 index 与节点数量 size/2 的大小,决定从头结点还是尾节点进行查找。
LinkedList的遍历操作
链表的遍历过程也很简单,和上面查找过程类似,我们从头节点往后遍历就行了。但对于 LinkedList 的遍历还是需要注意一些,不然可能会导致代码效率低下。通常情况下,我们会使用 foreach 遍历 LinkedList,而 foreach 最终转换成迭代器形式。所以分析 LinkedList 的遍历的核心就是它的迭代器实现。
| public ListIterator<E> listIterator(int index) { |
| checkPositionIndex(index); |
| return new ListItr(index); |
| } |
| |
| private class ListItr implements ListIterator<E> { |
| private Node<E> lastReturned; |
| private Node<E> next; |
| private int nextIndex; |
| private int expectedModCount = modCount; |
| |
| ListItr(int index) { |
| |
| next = (index == size) ? null : node(index); |
| nextIndex = index; |
| } |
| |
| public boolean hasNext() { |
| return nextIndex < size; |
| } |
| |
| public E next() { |
| checkForComodification(); |
| if (!hasNext()) |
| throw new NoSuchElementException(); |
| |
| lastReturned = next; |
| next = next.next; |
| nextIndex++; |
| return lastReturned.item; |
| } |
| |
| public boolean hasPrevious() { |
| return nextIndex > 0; |
| } |
| |
| public E previous() { |
| checkForComodification(); |
| if (!hasPrevious()) |
| throw new NoSuchElementException(); |
| |
| lastReturned = next = (next == null) ? last : next.prev; |
| nextIndex--; |
| return lastReturned.item; |
| } |
| |
| public int nextIndex() { |
| return nextIndex; |
| } |
| |
| public int previousIndex() { |
| return nextIndex - 1; |
| } |
| |
| public void remove() { |
| checkForComodification(); |
| if (lastReturned == null) |
| throw new IllegalStateException(); |
| |
| Node<E> lastNext = lastReturned.next; |
| unlink(lastReturned); |
| if (next == lastReturned) |
| next = lastNext; |
| else |
| nextIndex--; |
| lastReturned = null; |
| expectedModCount++; |
| } |
| |
| public void set(E e) { |
| if (lastReturned == null) |
| throw new IllegalStateException(); |
| checkForComodification(); |
| lastReturned.item = e; |
| } |
| |
| public void add(E e) { |
| checkForComodification(); |
| lastReturned = null; |
| if (next == null) |
| linkLast(e); |
| else |
| linkBefore(e, next); |
| nextIndex++; |
| expectedModCount++; |
| } |
| |
| public void forEachRemaining(Consumer<? super E> action) { |
| Objects.requireNonNull(action); |
| while (modCount == expectedModCount && nextIndex < size) { |
| action.accept(next.item); |
| lastReturned = next; |
| next = next.next; |
| nextIndex++; |
| } |
| checkForComodification(); |
| } |
| |
| final void checkForComodification() { |
| if (modCount != expectedModCount) |
| throw new ConcurrentModificationException(); |
| } |
| } |
LinkedList不擅长随机位置访问,如果使用随机访问遍历LinkedList效率会很差,如下:
| List<Integer> list = new LinkedList<>(); |
| list.add(1); |
| list.add(2); |
| for (int i = 0; i < list.size(); i++) { |
| int t = list.get(i); |
| } |
参考文档
LinkedList详解
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?