本文参考资料:html
一、《大话数据结构》java
二、http://blog.csdn.net/jzhf2012/article/details/8540543node
三、http://blog.csdn.net/jzhf2012/article/details/8540410算法
四、http://www.cnblogs.com/ITtangtang/p/3948610.html数组
五、http://blog.csdn.net/zw0283/article/details/51132161数据结构
原本在分析完HashSet、HashMap以后,我想紧跟着分析TreeMap以及TreeSet的,可是当我读过源码之后,我就放弃了这个想法。并非源码有多难,而是TreeMap涉及到的数据结构中的树结构,而我以前一直分析的都是线性结构,并且ArrayList、LinkedList也是线性结构,而且尚未分析。所以,我仍是决定循序渐进的进行,先把线性表所有分析完了,再去分析TreeMap。架构
ArrayList底层源码基本逻辑结构很简单,在《JDK学习---深刻理解java中的String》一文中基本已经分析完毕,惟一不一样的是String的底层数组不可变,而在ArrayList的底层Object[] 数组中,容许数组增、删、该操做,而且支持数组的动态扩容,这些东西不难,相信读者能很轻松搞明白这些知识,我就再也不说明了。post
本文我将重点的说明一下LinkedList知识点,而LinkedList的底层是一个双向链表结构,所以我会在解析源码以前,穿插一些双向链表的知识,而后结合代码进行分析。我不喜欢很空洞的单独去说数据结构,一是由于本人水平有限说不清楚,二是由于我以为理论须要结合代码,这样分析更加的直观一些。若是读者想要仔细的了解数据结构的知识,能够去找一些书籍详细研读。性能
双向链表学习
《JDK学习---深刻理解java中的String》一文介绍了数据结构的大致架构,《JDK学习---深刻理解java中的HashMap、HashSet底层实现》介绍了线性表的单链表。
本文将继续介绍数据结构的双向链表。
双向链表:在单链表的每一个节点中,再设置一个指向其前前驱节点的指针域 【DP】
既然是双向链表,那么对于链表中的某一个节点(p),它的后继的前驱,以及前驱的后继,其实都是这个节点自己:
p->next->prior = p = p->next-prior
双链表的插入操做并不复杂,可是顺序很重要,千万不能写错。
假设,咱们如今有一个节点s,它存储的元素为e,如今要将节点s插入到节点p和p->next之间,须要严格的遵照插入的前后顺序,以下图:
s -> prior = p; //把p赋值给s的前驱,如图中1 s -> next = p -> next; //把p -> next 赋值给s的后继,如图中2 p -> next -> prior = s; //把s 赋值给 p->next的前驱 ,如图中3 p -> next =s; //把s 赋值给p 的后继,如图中4
关键在于它们的顺序,因为第二、3步都用到了p->next , 若是第4步先执行,则会使得p->next提早变成了s,使得插入工做完成不了。口诀是:先搞定s的前驱和后继,再搞定后继的前驱,最后解决前节点的后继。
若是插入操做理解了,那么删除操做也就简单了。
p ->prior -> next = p -> next; //把p ->next赋值给p->prior的后继,如图中1 p ->next -> prior = p ->prior; //把 p ->prior赋值给p ->next 的前驱,如图中2 free(p); //释放节点p
总结:双向链表对于单链表而言,增、删操做要复杂一些,毕竟多了一个prior指针域,因此操做须要格外当心。另外,因为每一个节点都须要记录两份指针,空间相对而言也占用略多一些。不过,因为它良好的对称性,使得对某个节点的增、删操做带来了方便。说白了,就是用空间换时间。
LinkedList中的双向链表:
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; } }
链表的节点插入操做:
说实话,当我介绍完上面的双链表信息之后,我感受我已经把LinkedList介绍完了,由于LinkedList的底层源码确实太简单了,或者说是太规矩了,规矩到完彻底全的遵照链表的插入和删除操做的思路,一点点变化都没有,甚至比使用单链表+数组实现的HashMap还要简单。可能我说再多,都不如代码来的实在,下面进行代码分析:
add(E e) 方法:
public boolean add(E e) { linkLast(e); return true; }
这个方法没有逻辑判断,只是简单的调用linkLast(e)方法,下面继续跟进
void linkLast(E e) { final Node<E> l = last; //构造须要插入链表的节点元素,由于此方法是固定往链表尾部追加节点,所以每一个将要插入的节点都不存后继节点,或者说后继节点都为null; //此处在建立节点的时候,只是制定了当前节点的前驱以及当前节点的值域,由于后继节点为null,能够不指定后继指针域 final Node<E> newNode = new Node<>(l, e, null); last = newNode; //此处判断第一个节点是否存在,不存在的话直接将当前节点指定为头节点。若是存在,则将当前将要插入的节点指定给前一个节点的后继。所以是追加,这里能够省略当前节点的后继节点持有当前节点的指针 if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
add(int index, E element)方法:这个方法算可以体现出双链表节点的插入功能。
public void add(int index, E element) { checkPositionIndex(index); if (index == size) //这个地方在上面的方法已经分析过了,比较特殊,就再也不次分析了 linkLast(element); else linkBefore(element, node(index)); }
接下来跟进到linkBefore(element, node(index))方法中:
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++; }
咱们假设这个链表很长,咱们正常的在中间插入一个节点,也就是正常的双向链表节点插入功能。还记得口诀吗?口诀是:先搞定s的前驱和后继,再搞定后继的前驱,最后解决前节点的后继。
下面根据口诀,咱们对linkBefore(E e, Node<E> succ)进行解析: succ节点如今的位置,就是咱们须要插入的位置,也就是说要在succ节点和它的前一个节点succ->prev中间插入当前节点信息E e。
先搞定s的前驱和后继,此经过final Node<E> newNode = new Node<>(pred, e, succ)生成的newNode不就至关于以前口诀中的s吗,并且pred和succ不就是s的前驱和后继嘛。
再搞定后继的前驱,最后解决前节点的后继 : 此处不就是经过上面的succ.prev = newNode; 以及 后面的 pred.next = newNode; 来完成的嘛。
再次强调,这样分析,上面的黄色字体已经进行了假设了,即:这个链表很长,咱们正常的在中间插入一个节点,也就是正常的双向链表节点插入功能
链表的节点替换set(int index, E element) 方法:这个方法比较简单
public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
链表的节点删除remove(Object o)方法:
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 { //我想说明这个分支,由于链表的节点删除,确定是要模拟正常的状况,Object o这个参数正常存在 //在这个地方,我也是有疑问的。若是链表长度为1000,而咱们的o放在第998的位置上,若是是这样的话,for须要迭代998次,我彻底看不出来它的删除性能高在哪里。若是非要说性能高, //那只能勉强说仍是节省了移动节点的时间吧 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
看完这个方法,好像并无直接操做链表,下面看看unlink(Object)方法:
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; }
下面再来看看LinkedList的查询方法:
get(int index)方法:
public E get(int index) { //此处是判断给定的index下标是否合法 checkElementIndex(index); return node(index).item; }
跟进node(index)方法:
Node<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; } }
仔细分析一下此方法,不管是if分支,仍是else分支,都涉及到了for循环进行迭代,直至找到知足条件的index位置为止,这样的话若是数据量比较大,性能确定会比较低下。而ArrayList则是直接从底层数组中拿,不须要作任何的遍历,性能明显高不少。
再看看Iterator()方法:
public Iterator<E> iterator() { return listIterator(); }
跟进:
public ListIterator<E> listIterator() { return listIterator(0); }
再跟进:
public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index); return new ListItr(index); }
继续跟进ListItr类:
private class ListItr extends Itr implements ListIterator<E> {
继续跟进Itr:
public E next() { checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }
最终咱们发现,若是要是对LinkedList类进行迭代,最终仍是调用的get()方法,而这个方法咱们在上面已经分析过了,性能比ArrayList的get方法要低不少,所以LinkedList的Iterator()方法性能不高。
总结:
一、经过代码咱们看到,全部的插入方法,替换方法以及删除方法,都是直接对节点的前驱与后继进行直接操做,根本没有涉及到移动节点让出位置的状况,这个比线性表的顺序存储结构性能要高;不过须要说明的是,链表的性能要体如今数据量上面,好比咱们总共就10个节点元素,那么使用ArrayList与LinkedList的性能可能根本没有区别。
二、查询方面:ArrayList的get方法是直接到底层数组中去拿值,而LinkedList的get方法则每次都须要对链表进行遍历,尽管遍历的过程当中已经采用了算法进行优化,可是效率依旧仍是很低。
三、ArrayList的iterator底层依旧是本身的get()方法,而LinkedList的iterator方法底层也是本身的get()方法。而ArrayList的get方法性能比LinkedList的get方法性能高,所以,ArrayList的Iterator方法比LinkedList的iterator方法性能要高。总体来讲,ArrayList的查询性能就是比LinkedList的查询性能高