java数据结构-List

    技术2024-10-09  59

    List在数据结构中表现为是线性表的方式,其元素以线性方式存储,集合中允许存放重复的对象,List接口主要的实现类有ArrayList ArrayList其实就是一组长度可变的数组,当实例化了一个ArrayList,该数据也被实例化了,当向集合中添加对象时,数组的大小也随着改变,这样它所带来的有优点是快速的随机访问,即使访问每个元素所带来的性能问题也是很小的,但缺点就是想其中添加或删除对象速度慢,当你创建的数组是不确定其容量,所以当我们改变这个数组时就必须在内存中做很多的处理,如你想要数组中任意两个元素中间添加对象,那么在内存中数组要移动所有后面的对象。LinkedListLinkedList是通过节点的连接实现链表的数据结构,向linkedList中插入或删除元素的速度是特别快,而随机访问的速度相对较慢,这个是由于链表本身的性质造成的,在链表中,每个节点都包含了前一个节点的引用,后一个节点的引用和节点存储值,当一个新节点插入式,只需要修改其中相关的前后关系节点引用即可,删除节点也是一样。操作对象只需要改变节点的链接,新节点可以存放在内存的任何位置,但也就是因为如此LinkedList虽然存在get()方法,但是这个方法通过遍历节点来定位所以速度很慢。LinkedList还单独具addFrist(),addLast(),getFrist(),getLast(),removeFirst(),removeLast()方法,这些方法使得LinkedList可以作为堆栈,队列,和双队列来使用。说白了,ArrayList和LinkedList就是数据结构中的顺序存储表和链式存储表。ArrayList构造原理上面已经清楚ArrayList和LinkedList就是数据结构的顺序表和链表(不清楚的翻翻数据结构的书),下面简单分析一下它们的实现方式。下表是摘自sum提供的ArrayList的api文档ArrayList()          构造一个初始容量为 10 的空列表。ArrayList(Collection<? extends E> c)          构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。ArrayList(int initialCapacity)          构造一个具有指定初始容量的空列表。第一个构造函数是没有默认构建了一个初始容量10的空列表,第二个构造函数是制定collection元素的列表,第三个构造函数是由用户指定构造的列表初始化容量多少,如果使用第一个构造函数则表示默认调用该参数为initialCapacity=10来构造一个列表对象。ArrayList源码稍微进行分析

    public   class  ArrayList < E >   extends  AbstractList < E >          implements  List < E > , RandomAccess, Cloneable, java.io.Serializable{     private   static   final   long  serialVersionUID  =   8683452581122892189L ;     private   transient  Object[] elementData;     private   int  size;     public  ArrayList( int  initialCapacity) {     super ();         if  (initialCapacity  <   0 )             throw   new  IllegalArgumentException( " Illegal Capacity:  " +                                                initialCapacity);     this .elementData  =   new  Object[initialCapacity];    }     public  ArrayList() {     this ( 10 );    }     public  ArrayList(Collection <?   extends  E >  c) {    elementData  =  c.toArray();    size  =  elementData.length;     //  c.toArray might (incorrectly) not return Object[] (see 6260652)      if  (elementData.getClass()  !=  Object[]. class )        elementData  =  Arrays.copyOf(elementData, size, Object[]. class );    }     public   int  size() {     return  size;    }}

    可以发现ArrayList中包含两个主要的属性    private transient Object[] elementData;    private int size;其中elementData[]是列表的实现的核心数组,我们使用该数组来存放集合中的数据,而我们的构造函数所传递的initialCapacity参数是构建该数组的长度。在看看size的实现形式,它的作用是返回size的属性值的大小,我们再看看另外一个构造函数public ArrayList(Collection<? extends E> c),该构造函数的作用是把另外一个容器对象中的元素放入当点的List对象中。首先是通过调用另外一个容器对象c的size()来设置当前List对象的size属性的长度大小。接下来就似乎对elementData[]数组进行初始化,最后通过Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)方法把当前容器中的对象都存放进新的数组elementData,主要就完成了一个列表的创建。ArrayList容量扩充还有一个问题就是我们所建立的ArrayList是使用数组来实现的,但数组的长度一旦被初始化就不能改变,而我们在给此列表对象添加元素时却没有受到长度的限制,所以,ArrayList的elementData属性一定是存在一个动态扩充容量的机制,下面把相关的部分源码贴出来再做研究

    public   boolean  add(E e) {    ensureCapacity(size  +   1 );   //  Increments modCount!!     elementData[size ++ =  e;     return   true ;    }     protected   transient   int  modCount  =   0 ;         /**      * Increases the capacity of this <tt>ArrayList</tt> instance, if     * necessary, to ensure that it can hold at least the number of elements     * specified by the minimum capacity argument.     *     *  @param    minCapacity   the desired minimum capacity      */      public   void  ensureCapacity( int  minCapacity) {    modCount ++ ;     int  oldCapacity  =  elementData.length;     if  (minCapacity  >  oldCapacity) {        Object oldData[]  =  elementData;         int  newCapacity  =  (oldCapacity  *   3 ) / 2   +   1 ;             if  (newCapacity  <  minCapacity)        newCapacity  =  minCapacity;             //  minCapacity is usually close to size, so this is a win:             elementData  =  Arrays.copyOf(elementData, newCapacity);    }    }

    看看public boolean add(E e)方法,可以发现在添加一个元素到容器中的时候,我们会先通过ensureCapacity(size + 1)判断该数组是否需要扩充。public void ensureCapacity(int minCapacity)这个方法是用来判断当前的数组是否需要扩充,并且该扩充多少。modCount++; 表示当前的对象对elementData数组进行了多少次扩充,清空和移除等操作,相当于是一个对当前List对象的一个操作记录数。int oldCapacity = elementData.length; 初始化oldCapacity,表示为当前elementData数组的长度。if (minCapacity > oldCapacity) 判断minCapacity和oldCapacity谁大,来决定是否需要扩充。int newCapacity = (oldCapacity * 3)/2 + 1; 扩充的策列是判断(oldCapacity * 3)/2 + 1和minCapacity两者之间谁更大,取更大的数作为扩充后数组的initialCapacity值,然后使用数组拷贝的方式,把以前的数据转移到新的数组对象中如果minCapacity 小于 oldCapacity 就不需要再扩充。ArrayList删除元素

    public  E remove( int  index) {    RangeCheck(index);    modCount ++ ;    E oldValue  =  (E) elementData[index];     int  numMoved  =  size  -  index  -   1 ;     if  (numMoved  >   0 )        System.arraycopy(elementData, index + 1 , elementData, index,                 numMoved);    elementData[ -- size]  =   null //  Let gc do its work      return  oldValue;    }     private   void  RangeCheck( int  index) {     if  (index  >=  size)         throw   new  IndexOutOfBoundsException(         " Index:  " + index + " , Size:  " + size);    }

    在看看ArrayList移除元素是怎么实现的,首先判断需要删除的index是否在elementData数组的下标内,如不存在则抛出IndexOutOfBoundsException。modCount++; 与扩充元素一个,删除元素也记下来操作数。E oldValue = (E) elementData[index]; 获取需要删除元素的对象。int numMoved = size - index - 1; 获取需要被删除元素的下标,删除该元素后,数组需要在此元素下标后的所有对象进行内存的移动。System.arraycopy(elementData, index+1, elementData, index,numMoved);对numMoved后面的所有对象通过copy的方式进行内存的移动重新构建数组。说完ArrayList的实现,再说说linkedList构建双链表(LinkedList)LinkedList是类似于C语言的双链表,双链表比单链表多了一个域,这个双链表就有了三个域,一个域来用存储数据元素,一个用来指向后续节点,另一个是指向结点的直接前驱节点。

    public   class  LinkedList < E >      extends  AbstractSequentialList < E >      implements  List < E > , Deque < E > , Cloneable, java.io.Serializable{     private   transient  Entry < E >  header  =   new  Entry < E > ( null null null );     private   transient   int  size  =   0 ;     public  LinkedList() {        header.next  =  header.previous  =  header;    }     private   static   class  Entry < E >  {    E element;    Entry < E >  next;    Entry < E >  previous;    Entry(E element, Entry < E >  next, Entry < E >  previous) {         this .element  =  element;         this .next  =  next;         this .previous  =  previous;    }    }}

    在Entry类中,定义了三个属性,分别为E element 表示数据与,Entry<E> next为后续指针域,Entry<E> previous为前驱指针域。在LinkedList中定义了一个重要的属性header,头结点,不会纳入链表的总元素,该节点的previous是指向最后节点,next是指向第一节点。构造函数LinkedList() 构造一个空列表。将header的前驱指针域和后续指针域都指向了自己,看到这里可以发现,next和previous就是一个引用,其实也相等于C里面的指针,不过C不会处理空指针,直接放操作系统处理了,java就直接抛出NullPointerException,根本不让它破坏系统的机会。LinkedList元素变动上面说到了LinkedList的新增和删除的效率比ArrayList的高,实际上在 链表操作这些方法时,只需要改变2个节点各自的前驱指针和后续指针域,而ArrayList是需要移动很多的元素。

    public   boolean  add(E e) {    addBefore(e, header);         return   true ;    }     private  Entry < E >  addBefore(E e, Entry < E >  entry) {    Entry < E >  newEntry  =   new  Entry < E > (e, entry, entry.previous);    newEntry.previous.next  =  newEntry;    newEntry.next.previous  =  newEntry;    size ++ ;    modCount ++ ;     return  newEntry;    }     private  E remove(Entry < E >  e) {     if  (e  ==  header)         throw   new  NoSuchElementException();        E result  =  e.element;    e.previous.next  =  e.next;    e.next.previous  =  e.previous;        e.next  =  e.previous  =   null ;        e.element  =   null ;    size -- ;    modCount ++ ;         return  result;    }

    相比ArrayList的add()方法,LinkedList实现起来非常简单,主要是两行代码:newEntry.previous.next = newEntry;将上一节点的后续节点指向新增的节点newEntry.next.previous = newEntry;头节点的前驱节点指向新增节点,size和modCount自增记录。同样remove的实现也非常简单e.previous.next = e.next;该节点的后一节点的后去节点指向该节点的后驱节点,e.next.previous = e.previous;该节点的后一节点的前驱节点指向该节点的前驱节点。e.next = e.previous = null;把该节点的前驱节点和后驱节点全部指向null。e.element = null;把该节点的数据域设置为null。随机访问相比顺序表,链表的随机访问效率要低得多(理论说法,不是绝对),ArrayList可以根据索引号进行随机访问,而LinkedList则不要遍历访问。

    public  E get( int  index) {         return  entry(index).element;    }     private  Entry < E >  entry( int  index) {         if  (index  <   0   ||  index  >=  size)             throw   new  IndexOutOfBoundsException( " Index:  " + index +                                                  " , Size:  " + size);        Entry < E >  e  =  header;         if  (index  <  (size  >>   1 )) {             for  ( int  i  =   0 ; i  <=  index; i ++ )                e  =  e.next;        }  else  {             for  ( int  i  =  size; i  >  index; i -- )                e  =  e.previous;        }         return  e;    }

    上列的代码是对一个链表的遍历,其中包含了一个算法,如果给的索引号小于总节点数的一半,则在链表的前半部分第一个节点完进行遍历,如果给的索引号大于总节点数的一半,则从最后一个节点往前进行遍历直到索引号。最后总结一下ArrayList和LinkedList的各自特点1.ArrayList是基于线性表的顺序存储表,LinkedList是基本线性表的链表存储表。2.对于新增和删除元素,LinkedList比较占有优势,只需要变前后2个节点,而ArrayList要移动数据。3.对于随机访问来说,ArrayList比较占有优势,可以根据索引号快速访问,而LinkedList则需要遍历集合的元素来定位。4.而对于迭代操作(iterate)和查找操作(indexOf),两者是差不多。不过上面都是基于理论,具体问题还是要根据事实进行分析,如ArrayList删除的元素刚好是队列的最后一个元素,那么是无需要移动数据的,大体我们可以认为需要随机访问较多的那么比较适合用ArrayList,如果是插入和删除(如消息队列)较多的那么就需要考虑LinkedList。上面主要是参考了jdk源码,数据结构和一些相关资料本着好记性不如烂博客的精神记录下来,希望朋友们如果发觉哪里不对请指出来,虚心请教

    最新回复(0)