摘要:迭代器迭代器迭代中表被修改考虑以下这段代码在迭代器创建之后,对表进行了修改。这样设计是因为,迭代器代表表中某个元素的位置,内部会存储某些能够代表该位置的信息。
链表是对上一篇博文所说的顺序表的一种实现。
与ArrayList思路截然不同,链表的实现思路是:
不同元素实际上是存储在离散的内存空间中的。
每一个元素都有一个指针指向下一个元素,这样整个离散的空间就被“串”成了一个有顺序的表。
从链表的概念来讲,它可以算是一种递归的数据结构,因为链表拿掉第一个元素剩下的部分,依然构成一个链表。
时间空间复杂度通过索引定位其中的一个元素。由于不能像ArrayList那样直接通过计算内存地址偏移量来定位元素,只能从第一个元素开始顺藤摸瓜来数,因此为O(n)。
插入元素。实际上插入元素需要看情况:
如果指定链表中某个元素将其插之其后,那么首先得找出该元素对应的节点,还是O(n)。
如果能够直接指定节点往其后插入(如通过迭代器),那么仅仅需要移动指针即可完成,O(1)。
移除元素。移除和插入类似,得看提供的参数是什么。如果提供的是元素所在的节点,那么也只需要O(1)。
LinkedList的继承结构首先继承结构和ArrayList类似,实现了List接口。
但是,它继承的是继承了AbstractList类的AbstractSequentialList类,
这个类的作用也是给List中的部分函数提供默认实现,只是这个类对链表这种List的实现提供了更多贴合的默认函数实现。
还有可以注意到,LinkedList实现了Deque接口,这也很显然,链表这种结构天然就适合当做双端队列使用。
LinkedList源码分析 节点定义先来看链表的节点定义:
private static class Node{ E item; Node next; Node prev; Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; } }
可以看到,链表节点除了保存数据外,还需要保存指向前后节点的指针。
这里,链表即有后继指针也有前驱指针,因此这是一个双向链表。
一组节点之间按顺序用指针指起来,就形成了链表的链状结构。
属性和构造函数transient int size = 0; transient Nodefirst; transient Node last;
三个属性,first和last分别指向链条的首节点和尾节点。
这样有个好处,就是链表即可以使用头插法也可以采用尾插法。
size属性跟踪了链表的元素个数。虽然说遍历一遍链表也能统计到元素个数,
但是那是O(n)的费时操作。
因此,我们可以发现链表的size方法是O(1)的时间复杂度。
public LinkedList() { }
LinkedList的代码很简单,构造函数空空如也。
空表中,first和last字段都为null。
public E get(int index) { checkElementIndex(index); return node(index).item; } public E set(int index, E element) { checkElementIndex(index); Nodex = node(index); E oldVal = x.item; x.item = element; return oldVal; } Node node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
get和set的思路都是先根据索引定位到链表节点,然后获得或设置节点中的数据,这抽象出了node函数,根据索引找到链表节点。
node的思路也很显然,遍历一遍即可得到。
这里做了一点优化,我们可以发现LinkedList的实现是一个双向链表,并且LinkedList持有了头尾指针。
那么,根据索引和size就可以知道该节点是在链表的前半部分还是后半部分,
从而决定从头节点开始遍历还是从尾节点开始遍历,这样最多遍历 N / 2次即可找到。
public boolean add(E e) { linkLast(e); return true; } public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } void linkLast(E e) { final Nodel = last; final Node newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } void linkBefore(E e, Node succ) { final Node pred = succ.prev; final Node newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
添加/删除的思路都类似,删除的代码就不贴了。如果能够提供需要被操作的节点,就能直接移动下指针,O(1)完成。否则就需要遍历找到这个节点再操作。
需要关注两点:
有的操作是操作头指针,有的操作是操作尾指针。但是不管操作哪一个,都需要维护另外一个指针及size的值。
如果是删除,删除后及时把相关节点的item字段置为null,以帮助gc能更快的释放内存。
modCount字段分析之前阅读ArrayList的代码时发现了modCount这一字段,它是定义在AbstractList类中的。之前不知道它起到什么作用,这次给弄明白了。
迭代器 迭代器迭代中表被修改考虑以下这段代码:
Listlist = new LinkedList<>(); Iterator it = list.listIterator(); list.add(1); it.next();
在迭代器创建之后,对表进行了修改。这时候如果操作迭代器,则会得到异常java.util.ConcurrentModificationException。
这样设计是因为,迭代器代表表中某个元素的位置,内部会存储某些能够代表该位置的信息。当表发生改变时,该信息的含义可能会发生变化,这时操作迭代器就可能会造成不可预料的事情。
因此,果断抛异常阻止,是最好的方法。
实际上,这种迭代器迭代过程中表结构发生改变的情况,更经常发生在多线程的环境中。
记录表被修改的标记这种机制的实现就需要记录表被修改,那么思路是使用状态字段modCount。
每当会修改表的操作执行时,都将此字段加1。使用者只需要前后对比该字段就知道中间这段时间表是否被修改。
如linkedList中的头插和尾插函数,就将modCount字段自增:
private void linkFirst(E e) { final Node迭代器f = first; final Node newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } void linkLast(E e) { final Node l = last; final Node newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
迭代器使用该字段来判断,
private class ListItr implements ListIterator{ private Node lastReturned; private Node next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(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 void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
迭代器开始时记录下初始的值:
private int expectedModCount = modCount;
然后与现在的值对比判断是否被修改:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
这是一个内部类,隐式持有LinkedList的引用,能够直接访问到LinkedList中的modCount字段。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/71569.html
摘要:我们来看相关源码我们看到封装的和操作其实就是对头结点的操作。迭代器通过指针,能指向下一个节点,无需做额外的遍历,速度非常快。不同的遍历性能差距极大,推荐使用迭代器进行遍历。LinkedList类介绍 上一篇文章我们介绍了JDK中ArrayList的实现,ArrayList底层结构是一个Object[]数组,通过拷贝,复制等一系列封装的操作,将数组封装为一个几乎是无限的容器。今天我们来介绍JD...
摘要:我们来看相关源码我们看到封装的和操作其实就是对头结点的操作。迭代器通过指针,能指向下一个节点,无需做额外的遍历,速度非常快。不同的遍历性能差距极大,推荐使用迭代器进行遍历。LinkedList类介绍 上一篇文章我们介绍了JDK中ArrayList的实现,ArrayList底层结构是一个Object[]数组,通过拷贝,复制等一系列封装的操作,将数组封装为一个几乎是无限的容器。今天我们来介绍JD...
摘要:介绍是线程不安全的,允许元素为的双向链表。构造方法共有两个构造方法,一个是创建一个空的构造函数,一个是将已有的添加到中。是将元素插入到的头部。下一篇文章继续分析上次分析了的结构和添加方法这次开始分析下面的。注意源码版本为直接进入正题。 如果本文中有不正确的地方请指出由于没有留言可以在公众号添加我的好友共同讨论。 1.介绍 LinkedList 是线程不安全的,允许元素为null的双向链...
阅读 1368·2021-09-13 10:25
阅读 552·2019-08-30 15:53
阅读 2265·2019-08-30 15:44
阅读 2024·2019-08-29 17:20
阅读 1593·2019-08-29 16:36
阅读 1794·2019-08-29 14:10
阅读 1784·2019-08-29 12:44
阅读 1165·2019-08-23 14:13