深入Collection之LinkedList
来源:互联网 发布:scx4321扫描软件 编辑:程序博客网 时间:2024/06/02 04:09
这一篇是有关LinkedList的学习,那么闲话不多扯,直接按照上一篇的博文的模式来分析LinkedList的实现和功能。
成员变量
transient int size = 0; /** * 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; 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; } }
由定义的成员变量和Node类的结构可以看出LinkedList是以双向链表为实现形式的List结构。
构造方法
无参构造
public LinkedList() { }
没有任何其他的操作,仅仅构造一个空的List。
参数为Collection的构造
public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
很显然,先构造一个空List,然后执行addAll(c)方法,将Collection c添加入List。关于addAll()方法的介绍请看下文。
成员方法
因为对LinkedList的操作均是传递到对双向链表的操作,所以先来看对链表的基础操作:
操作双向链表方法
添加第一个节点
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++; }
先将f指向原来初始节点对象对应的内存空间,再新建新的初始节点,定义前置为null,内容为e,后置为f(原来的初始节点),同时令first指向新建的初始节点的内存空间。
因此如果之前没有初始节点,只需要将last指向newNode即可。如果有初始节点,将初始节点f的prev指向newNode即可完成。再将List大小+1,更改次数+1.
作为最后一个元素节点添加
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++; }
插入过程与第一个很类似。先将 l 指向原来的终节点,再新建一个新的终节点,并将last指向新建的终节点。如果原来没有终节点,则将first也指向新建节点,此时链表中只有一个节点。若原来有终节点,将原来终节点的后置节点指向新建的终节点,完成连接。最后将表大小+1,更改次数+1.
解绑链表的第一个节点
private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
从链表角度来看,解绑初始节点,无非是将first节点指向第二个节点,再将第二个节点next的prev设为null。而java实现中,需要将初始节点f的item和next设为null,以便对象f没有对象引用方便GC。当只有链表只有一个节点时,需要将last设为null。
解绑链表的最后一个节点
private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; }
同样从双向链表的角度来看,唯一值得关注的操作就是让l的item引用和prev引用为null,方便GC。其他操作是将last指向prev节点。如果prev为null,则将first设为null,否则另prev的后置节点为null。
在一个节点前插入一个元素
void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
定义succ之前的前置节点为pred,新建插入节点。将succ的前置节点设为新建节点。如果succ没有前置节点,则将first指向新建节点;若有,则将pred的后置节点指向新建节点newNode。
移除链表中一个非空节点
E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final 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; }
移除节点x,定义x的前置节点为prev,后置节点为next。正常移除时需要将prev的后置引用指向next,next的前置引用指向prev。如果prev或next为空,则将first指向next或last指向prev。同时清除x残留的引用。大小-1,更改次数+1.
操作LinkedList方法
常用方法
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; }public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); }public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); }public void addFirst(E e) { linkFirst(e); }public void addLast(E e) { linkLast(e); }public int size() { return size; }public boolean add(E e) { linkLast(e); return true; }public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }public E get(int index) { checkElementIndex(index); return node(index).item; } public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
以上方法常用,且操作简单,都是对链表的简单操作。因此在这不加多余的解释,相信大家也能理解。
addAll
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; }//将LinkedList转化成数组,实际实现是遍历链表,将链表中的值存入数组内。public Object[] toArray() { Object[] result = new Object[size]; int i = 0; for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; return result; }//找到处于index位置的节点,if-else判断是为了找出耗时较少的遍历途径,返回NodeNode<E> node(int index) { // assert isElementIndex(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; } }
作为addAll方法,实现的本质还是在链表的index位置插入内容为c的链表。定义succ为当前插入位置的节点,pred为插入位置的前置节点。插入过程就是遍历内容数组,新建一个节点,将pred的后置节点指向新建节点newNode。完成后,后移pred指向新建节点。数组循环完成后,建立最后一个新建节点和插入位置节点succ的关联。至此,操作完成。
而其他的成员方法,都是对链表的操作,均可以调用操作链表的方法,所以不再一一介绍,如果有兴趣可以自己查看源码。由于LinkedList采用双向链表实现,所以不存在数组扩容的问题。通过以上方法可以看出,LinkedList的增删操作开销很小,只需要遍历到目标位置,进行链表节点的增删,而对于在表起点和终点的增删,更只是常数时间的操作。但相较于ArrayList的get方法,LinkedList仍需要遍历链表才能找到索引对应值,效率不好。
迭代器的实现
想要在不暴露LinkedList的内部实现的前提下,遍历访问List使用迭代器是一种很好的选择。接下来就是介绍LinkedList中的迭代器实现:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{ //step 4 public ListIterator<E> listIterator(int index) { checkPositionIndex(index); return new ListItr(index); } }
public abstract class AbstractSequentialList<E> extends AbstractList<E> { //step 1 public Iterator<E> iterator() { return listIterator(); } //step 3 public abstract ListIterator<E> listIterator(int index); }
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { public Iterator<E> iterator() { return new Itr(); } //step 2 public ListIterator<E> listIterator() { return listIterator(0); } public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index); return new ListItr(index); }}
按照以上的执行顺序可以看到,LinkedList的iterator()的实现最终追溯到内部类ListItr。
private class ListItr implements ListIterator<E> { private Node<E> lastReturned = null;//可以理解为当前访问的节点 private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } //判断是否有下个元素,将下个元素索引值和List大小比较 public boolean hasNext() { return nextIndex < size; } //返回索引指向的元素,并将next指向当前的后置节点,索引值+1。 public E next() { checkForComodification();//迭代器和初始List一致性校验 if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex > 0; } //返回当前next节点指向的前置节点,并将next指向当前的前置节点,索引值-1. //主要应用于定义的另一种倒叙遍历方式 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++; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
既然提到迭代遍历,也可以顺带讨论List遍历时删除元素的问题:
public static List<String> initialList(){ List<String> list = new LinkedList<>(); list.add("a"); list.add("b"); list.add("c"); list.add("d"); return list; } public static void removeByFor(List<String> list){ for (int i = 0; i < list.size(); i++) { if("b".equals(list.get(i))){ System.out.println(i); list.remove(i); } if("d".equals(list.get(i))){ System.out.println(i); list.remove(i); } } System.out.println(list); } public static void removeByForeach(List<String> list){ for (String string : list) { if ("b".equals(string)) { list.remove(string); } } System.out.println(list); } public static void removeByIterator(List<String>list){ Iterator<String> e = list.iterator(); while (e.hasNext()) { String item = e.next(); if ("b".equals(item)) { e.remove(); } } System.out.println(list); } public static void main(String []args){ removeByFor(initialList()); //1 removeByForeach(initialList()); //2 removeByIterator(initialList()); //3 }
- 方法1:能正常删除,但是删除d时,索引index为2,和原List中的索引值不同,可能会对其他操作造成影响。且删除操作经历了两次遍历(外部一次,remove操作一次),时间复杂度增加。
- 方法2:会抛出ConcurrentModificationException异常。因为foreach内部也是使用iterator进行遍历,方法2操作只更改了modCount,没有更改Iterator中的expectedModCount。
- 方法3:最合适的遍历删除,只经历一次遍历,时间复杂度低。
结语
LinkedList的实现是基于双向链表,因此它的一切性质都是与双向链表相关的:
- get(int index) O(n) 由于双向链表需要依次遍历到目标index,所以get效率不如ArrayList。
- add(E e) O(1) 添加到链表尾部,只需要建立关联即可。
- add(int index,E e) O(n) 由于需要遍历到目标index,再添加节点建立关联。但是效率比ArrayList的移动数组好了不少。
- remove(int index) O(n) 和add方法类似,耗时主要在遍历到目标index。整体效率比ArrayList的remove()方法好。
- 深入Collection之LinkedList
- Collection探究之LinkedList
- Collection框架之LinkedList
- Collection容器初探之LinkedList
- 深入Collection之ArrayList
- 深入Collection之HashMap
- collection接口之Arraylist,vector,Linkedlist
- Collection之List Vector ArrayList and linkedList
- Java Collection Framework 之 LinkedList 源码解析
- JAVA源码分析Collection之LinkedList
- Java深入源码之LinkedList
- 深入学习Java之LinkedList
- Java基础之集合框架(一)--Collection、List、LinkedList、HashSet
- Java基础之集合框架(一)--Collection、List、LinkedList、HashSet
- 【java编程】Collection类之LinkedList实现队列,堆栈
- 深入Java集合系列之二:LinkedList
- Collection(二)--------linkedList
- 【java】【java Collection】LinkedList
- linux 驱动程序 端口通信
- EasyUI入门
- Axure RP 8最新激活码
- 基于Spring的QQ第三方登录实现
- 【CC2640R2】SIMPLELINK_CC2640R2_SDK 1.30.00.25发行说明
- 深入Collection之LinkedList
- 【敏捷开发每日一贴】敏捷实践Showcase的七宗罪
- 通达OA 开发工作流触发器碰到的一个特殊现象
- 学习Linux的决心书
- zookeeper安装及使用
- 去除红色字体
- lintcode(501)迷你推特
- adb操作命令详解及大全
- 欢迎使用CSDN-markdown编辑器