深入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结构。

构造方法

  1. 无参构造

    public LinkedList() {   }

    没有任何其他的操作,仅仅构造一个空的List。

  2. 参数为Collection的构造

    public LinkedList(Collection<? extends E> c) {       this();       addAll(c);   }

    很显然,先构造一个空List,然后执行addAll(c)方法,将Collection c添加入List。关于addAll()方法的介绍请看下文。

成员方法

因为对LinkedList的操作均是传递到对双向链表的操作,所以先来看对链表的基础操作:

操作双向链表方法

  1. 添加第一个节点

    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.

  2. 作为最后一个元素节点添加

    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.

  3. 解绑链表的第一个节点

    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。

  4. 解绑链表的最后一个节点

    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。

  5. 在一个节点前插入一个元素

    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。

  6. 移除链表中一个非空节点

    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()方法好。
0 0
原创粉丝点击