上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点。本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于双向链表实现的,因此它具有插入删除快而查找修改慢的特点,此外,通过对双向链表的操作还可以实现队列和栈的功能。LinkedList的底层结构如下图所示。
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 7 Node(Node<E> prev, E element, Node<E> next) { 8 this.item = element; 9 this.next = next; 10 this.prev = prev; 11 } 12 }
Node这个内部类其实很简单,只有三个成员变量和一个构造器,item表示结点的值,next为下一个结点的引用,prev为上一个结点的引用,通过构造器传入这三个值。接下来再看看LinkedList的成员变量和构造器。
1 //集合元素个数 2 transient int size = 0; 3 4 //头结点引用 5 transient Node<E> first; 6 7 //尾节点引用 8 transient Node<E> last; 9 10 //无参构造器 11 public LinkedList() {} 12 13 //传入外部集合的构造器 14 public LinkedList(Collection<? extends E> c) { 15 this(); 16 addAll(c); 17 }
LinkedList持有头结点的引用和尾结点的引用,它有两个构造器,一个是无参构造器,一个是传入外部集合的构造器。与ArrayList不同的是LinkedList没有指定初始大小的构造器。看看它的增删改查方法。
1 //增(添加) 2 public boolean add(E e) { 3 //在链表尾部添加 4 linkLast(e); 5 return true; 6 } 7 8 //增(插入) 9 public void add(int index, E element) { 10 checkPositionIndex(index); 11 if (index == size) { 12 //在链表尾部添加 13 linkLast(element); 14 } else { 15 //在链表中部插入 16 linkBefore(element, node(index)); 17 } 18 } 19 20 //删(给定下标) 21 public E remove(int index) { 22 //检查下标是否合法 23 checkElementIndex(index); 24 return unlink(node(index)); 25 } 26 27 //删(给定元素) 28 public boolean remove(Object o) { 29 if (o == null) { 30 for (Node<E> x = first; x != null; x = x.next) { 31 if (x.item == null) { 32 unlink(x); 33 return true; 34 } 35 } 36 } else { 37 //遍历链表 38 for (Node<E> x = first; x != null; x = x.next) { 39 if (o.equals(x.item)) { 40 //找到了就删除 41 unlink(x); 42 return true; 43 } 44 } 45 } 46 return false; 47 } 48 49 //改 50 public E set(int index, E element) { 51 //检查下标是否合法 52 checkElementIndex(index); 53 //获取指定下标的结点引用 54 Node<E> x = node(index); 55 //获取指定下标结点的值 56 E oldVal = x.item; 57 //将结点元素设置为新的值 58 x.item = element; 59 //返回之前的值 60 return oldVal; 61 } 62 63 //查 64 public E get(int index) { 65 //检查下标是否合法 66 checkElementIndex(index); 67 //返回指定下标的结点的值 68 return node(index).item; 69 }
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 //卸载指定结点 25 E unlink(Node<E> x) { 26 //获取给定结点的元素 27 final E element = x.item; 28 //获取给定结点的下一个结点的引用 29 final Node<E> next = x.next; 30 //获取给定结点的上一个结点的引用 31 final Node<E> prev = x.prev; 32 33 //如果给定结点的上一个结点为空, 说明给定结点为头结点 34 if (prev == null) { 35 //将头结点引用指向给定结点的下一个结点 36 first = next; 37 } else { 38 //将上一个结点的后继结点引用指向给定结点的后继结点 39 prev.next = next; 40 //将给定结点的上一个结点置空 41 x.prev = null; 42 } 43 44 //如果给定结点的下一个结点为空, 说明给定结点为尾结点 45 if (next == null) { 46 //将尾结点引用指向给定结点的上一个结点 47 last = prev; 48 } else { 49 //将下一个结点的前继结点引用指向给定结点的前继结点 50 next.prev = prev; 51 x.next = null; 52 } 53 54 //将给定结点的元素置空 55 x.item = null; 56 //集合元素个数减一 57 size--; 58 //修改次数加一 59 modCount++; 60 return element; 61 }
linkBefore和unlink是具有代表性的链接结点和卸载结点的操作,其他的链接和卸载两端结点的方法与此类似,所以我们重点介绍linkBefore和unlink方法。 linkBefore方法的过程图:
unlink方法的过程图:
通过上面图示看到对链表的插入和删除操作的时间复杂度都是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 //获取头结点 8 public E element() { 9 return getFirst(); 10 } 11 12 //弹出头结点 13 public E poll() { 14 final Node<E> f = first; 15 return (f == null) ? null : unlinkFirst(f); 16 } 17 18 //移除头结点 19 public E remove() { 20 return removeFirst(); 21 } 22 23 //在队列尾部添加结点 24 public boolean offer(E e) { 25 return add(e); 26 }
双向队列操作:
1 //在头部添加 2 public boolean offerFirst(E e) { 3 addFirst(e); 4 return true; 5 } 6 7 //在尾部添加 8 public boolean offerLast(E e) { 9 addLast(e); 10 return true; 11 } 12 13 //获取头结点 14 public E peekFirst() { 15 final Node<E> f = first; 16 return (f == null) ? null : f.item; 17 } 18 19 //获取尾结点 20 public E peekLast() { 21 final Node<E> l = last; 22 return (l == null) ? null : l.item; 23 }
栈操作:
1 //入栈 2 public void push(E e) { 3 addFirst(e); 4 } 5 6 //出栈 7 public E pop() { 8 return removeFirst(); 9 }
不管是单向队列还是双向队列还是栈,其实都是对链表的头结点和尾结点进行操作,它们的实现都是基于addFirst(),addLast(),removeFirst(),removeLast()这四个方法,它们的操作和linkBefore()和unlink()类似,只不过一个是对链表两端操作,一个是对链表中间操作。可以说这四个方法都是linkBefore()和unlink()方法的特殊情况,因此不难理解它们的内部实现,在此不多做介绍。到这里,我们对LinkedList的分析也即将结束,对全文中的重点做个总结: 1. LinkedList是基于双向链表实现的,不论是增删改查方法还是队列和栈的实现,都可通过操作结点实现 2. LinkedList无需提前指定容量,因为基于链表操作,集合的容量随着元素的加入自动增加 3. LinkedList删除元素后集合占用的内存自动缩小,无需像ArrayList一样调用trimToSize()方法 4. LinkedList的所有方法没有进行同步,因此它也不是线程安全的,应该避免在多线程环境下使用 5. 以上分析基于JDK1.7,其他版本会有些出入,因此不能一概而论