集合系列—LinkedList源码分析

野性酷女 2022-04-12 11:22 441阅读 0赞

上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点。本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于双向链表实现的,因此它具有插入删除快而查找修改慢的特点,此外,通过对双向链表的操作还可以实现队列和栈的功能。

LinkedList的底层结构如下图所示。

640_wx_fmt_png

F表示头结点引用,L表示尾结点引用,链表的每个结点都有三个元素,分别是前继结点引用(P),结点元素的值(E),后继结点的引用(N)。结点由内部类Node表示,我们看看它的内部结构。

  1. //结点内部类
  2. private static class Node<E> {
  3. E item; //元素
  4. Node<E> next; //下一个节点
  5. Node<E> prev; //上一个节点
  6. Node(Node<E> prev, E element, Node<E> next) {
  7. this.item = element;
  8. this.next = next;
  9. this.prev = prev;
  10. }
  11. }

Node这个内部类其实很简单,只有三个成员变量和一个构造器,item表示结点的值,next为下一个结点的引用,prev为上一个结点的引用,通过构造器传入这三个值。接下来再看看LinkedList的成员变量和构造器。

  1. //集合元素个数
  2. transient int size = 0;
  3. //头结点引用
  4. transient Node<E> first;
  5. //尾节点引用
  6. transient Node<E> last;
  7. //无参构造器
  8. public LinkedList() {}
  9. //传入外部集合的构造器
  10. public LinkedList(Collection<? extends E> c) {
  11. this();
  12. addAll(c);
  13. }

LinkedList持有头结点的引用和尾结点的引用,它有两个构造器,一个是无参构造器,一个是传入外部集合的构造器。与ArrayList不同的是LinkedList没有指定初始大小的构造器。看看它的增删改查方法。

  1. //增(添加)
  2. public boolean add(E e) {
  3. //在链表尾部添加
  4. linkLast(e);
  5. return true;
  6. }
  7. //增(插入)
  8. public void add(int index, E element) {
  9. checkPositionIndex(index);
  10. if (index == size) {
  11. //在链表尾部添加
  12. linkLast(element);
  13. } else {
  14. //在链表中部插入
  15. linkBefore(element, node(index));
  16. }
  17. }
  18. //删(给定下标)
  19. public E remove(int index) {
  20. //检查下标是否合法
  21. checkElementIndex(index);
  22. return unlink(node(index));
  23. }
  24. //删(给定元素)
  25. public boolean remove(Object o) {
  26. if (o == null) {
  27. for (Node<E> x = first; x != null; x = x.next) {
  28. if (x.item == null) {
  29. unlink(x);
  30. return true;
  31. }
  32. }
  33. } else {
  34. //遍历链表
  35. for (Node<E> x = first; x != null; x = x.next) {
  36. if (o.equals(x.item)) {
  37. //找到了就删除
  38. unlink(x);
  39. return true;
  40. }
  41. }
  42. }
  43. return false;
  44. }
  45. //改
  46. public E set(int index, E element) {
  47. //检查下标是否合法
  48. checkElementIndex(index);
  49. //获取指定下标的结点引用
  50. Node<E> x = node(index);
  51. //获取指定下标结点的值
  52. E oldVal = x.item;
  53. //将结点元素设置为新的值
  54. x.item = element;
  55. //返回之前的值
  56. return oldVal;
  57. }
  58. //查
  59. public E get(int index) {
  60. //检查下标是否合法
  61. checkElementIndex(index);
  62. //返回指定下标的结点的值
  63. return node(index).item;
  64. }

LinkedList的添加元素的方法主要是调用linkLast和linkBefore两个方法,linkLast方法是在链表后面链接一个元素,linkBefore方法是在链表中间插入一个元素。LinkedList的删除方法通过调用unlink方法将某个元素从链表中移除。下面我们看看链表的插入和删除操作的核心代码。

  1. //链接到指定结点之前
  2. void linkBefore(E e, Node<E> succ) {
  3. //获取给定结点的上一个结点引用
  4. final Node<E> pred = succ.prev;
  5. //创建新结点, 新结点的上一个结点引用指向给定结点的上一个结点
  6. //新结点的下一个结点的引用指向给定的结点
  7. final Node<E> newNode = new Node<>(pred, e, succ);
  8. //将给定结点的上一个结点引用指向新结点
  9. succ.prev = newNode;
  10. //如果给定结点的上一个结点为空, 表明给定结点为头结点
  11. if (pred == null) {
  12. //将头结点引用指向新结点
  13. first = newNode;
  14. } else {
  15. //否则, 将给定结点的上一个结点的下一个结点引用指向新结点
  16. pred.next = newNode;
  17. }
  18. //集合元素个数加一
  19. size++;
  20. //修改次数加一
  21. modCount++;
  22. }
  23. //卸载指定结点
  24. E unlink(Node<E> x) {
  25. //获取给定结点的元素
  26. final E element = x.item;
  27. //获取给定结点的下一个结点的引用
  28. final Node<E> next = x.next;
  29. //获取给定结点的上一个结点的引用
  30. final Node<E> prev = x.prev;
  31. //如果给定结点的上一个结点为空, 说明给定结点为头结点
  32. if (prev == null) {
  33. //将头结点引用指向给定结点的下一个结点
  34. first = next;
  35. } else {
  36. //将上一个结点的后继结点引用指向给定结点的后继结点
  37. prev.next = next;
  38. //将给定结点的上一个结点置空
  39. x.prev = null;
  40. }
  41. //如果给定结点的下一个结点为空, 说明给定结点为尾结点
  42. if (next == null) {
  43. //将尾结点引用指向给定结点的上一个结点
  44. last = prev;
  45. } else {
  46. //将下一个结点的前继结点引用指向给定结点的前继结点
  47. next.prev = prev;
  48. x.next = null;
  49. }
  50. //将给定结点的元素置空
  51. x.item = null;
  52. //集合元素个数减一
  53. size--;
  54. //修改次数加一
  55. modCount++;
  56. return element;
  57. }

linkBefore和unlink是具有代表性的链接结点和卸载结点的操作,其他的链接和卸载两端结点的方法与此类似,所以我们重点介绍linkBefore和unlink方法。

linkBefore方法的过程图:

640_wx_fmt_png 1

unlink方法的过程图:

640_wx_fmt_png 2

通过上面图示看到对链表的插入和删除操作的时间复杂度都是O(1),而对链表的查找和修改操作都需要遍历链表进行元素的定位,这两个操作都是调用的node(int index)方法定位元素,看看它是怎样通过下标来定位元素的。

  1. //根据指定位置获取结点
  2. Node<E> node(int index) {
  3. //如果下标在链表前半部分, 就从头开始查起
  4. if (index < (size >> 1)) {
  5. Node<E> x = first;
  6. for (int i = 0; i < index; i++) {
  7. x = x.next;
  8. }
  9. return x;
  10. } else {
  11. //如果下标在链表后半部分, 就从尾开始查起
  12. Node<E> x = last;
  13. for (int i = size - 1; i > index; i--) {
  14. x = x.prev;
  15. }
  16. return x;
  17. }
  18. }

通过下标定位时先判断是在链表的上半部分还是下半部分,如果是在上半部分就从头开始找起,如果是下半部分就从尾开始找起,因此通过下标的查找和修改操作的时间复杂度是O(n/2)。通过对双向链表的操作还可以实现单项队列,双向队列和栈的功能。

单向队列操作:

  1. //获取头结点
  2. public E peek() {
  3. final Node<E> f = first;
  4. return (f == null) ? null : f.item;
  5. }
  6. //获取头结点
  7. public E element() {
  8. return getFirst();
  9. }
  10. //弹出头结点
  11. public E poll() {
  12. final Node<E> f = first;
  13. return (f == null) ? null : unlinkFirst(f);
  14. }
  15. //移除头结点
  16. public E remove() {
  17. return removeFirst();
  18. }
  19. //在队列尾部添加结点
  20. public boolean offer(E e) {
  21. return add(e);
  22. }

双向队列操作:

  1. //在头部添加
  2. public boolean offerFirst(E e) {
  3. addFirst(e);
  4. return true;
  5. }
  6. //在尾部添加
  7. public boolean offerLast(E e) {
  8. addLast(e);
  9. return true;
  10. }
  11. //获取头结点
  12. public E peekFirst() {
  13. final Node<E> f = first;
  14. return (f == null) ? null : f.item;
  15. }
  16. //获取尾结点
  17. public E peekLast() {
  18. final Node<E> l = last;
  19. return (l == null) ? null : l.item;
  20. }

栈操作:

  1. //入栈
  2. public void push(E e) {
  3. addFirst(e);
  4. }
  5. //出栈
  6. public E pop() {
  7. return removeFirst();
  8. }

不管是单向队列还是双向队列还是栈,其实都是对链表的头结点和尾结点进行操作,它们的实现都是基于addFirst(),addLast(),removeFirst(),removeLast()这四个方法,它们的操作和linkBefore()和unlink()类似,只不过一个是对链表两端操作,一个是对链表中间操作。可以说这四个方法都是linkBefore()和unlink()方法的特殊情况,因此不难理解它们的内部实现,在此不多做介绍。到这里,我们对LinkedList的分析也即将结束,对全文中的重点做个总结:

  • LinkedList是基于双向链表实现的,不论是增删改查方法还是队列和栈的实现,都可通过操作结点实现
  • LinkedList无需提前指定容量,因为基于链表操作,集合的容量随着元素的加入自动增加
  • LinkedList删除元素后集合占用的内存自动缩小,无需像ArrayList一样调用trimToSize()方法
  • LinkedList的所有方法没有进行同步,因此它也不是线程安全的,应该避免在多线程环境下使用
  • 以上分析基于JDK1.7,其他版本会有些出入,因此不能一概而论。

发表评论

表情:
评论列表 (有 0 条评论,441人围观)

还没有评论,来说两句吧...

相关阅读