关键词:
一、什么是顺序表
在了解顺序表之前,需要先了解什么是顺序表?顺序表是线性表的一种,线性表分为顺序表和链表。其中顺序表在java中又分为数组(最简单的顺序表)和ArrayList。为什么我们称其为顺序表呢?原因顾名思义是该数据结构在逻辑上和物理结构上的存储顺序均是连续的。下面,我们就以一张图来说明什么是顺序表。
这个图代表逻辑上的9个元素,每一个位置均存储一个数据,数据存储在内存中时的物理地址也是连续的。
下面我们就介绍一下ArrayList的基本操作,增删改查。
二、ArrayList的数据格式
在此篇文章中,我们只讲ArrayList,数组不再讲解。ArrayList为什么说是顺序表呢?其原因我们可以看到ArrayList内部维护的其实还是一个缓存数组,所有的操作都是基于该数组进行实现。看源码:
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access
三、ArrayList的扩容
ArrayList有一套自己的扩容机制,在ArrayList初始化完成之后,如果不指定容量,则缓存容量默认为10.
1 /** 2 * Default initial capacity. 3 */ 4 private static final int DEFAULT_CAPACITY = 10;
在每次add操作时,会进行扩容计算,整个流程源码如下:
1 ensureCapacityInternal(size + 1); // Increments modCount!! 添加元素时调用扩容 2 3 private void ensureCapacityInternal(int minCapacity) 4 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 5 6 7 private static int calculateCapacity(Object[] elementData, int minCapacity) // 得到最小的扩容量 8 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 9 return Math.max(DEFAULT_CAPACITY, minCapacity); // 获取默认的容量和传入参数较大的一个值 10 11 return minCapacity; 12 13 14 private void ensureExplicitCapacity(int minCapacity) 15 modCount++; 16 17 // overflow-conscious code 18 if (minCapacity - elementData.length > 0) 19 grow(minCapacity); // 扩容开始 20 21 22 /** 23 * Increases the capacity to ensure that it can hold at least the 24 * number of elements specified by the minimum capacity argument. 25 * 26 * @param minCapacity the desired minimum capacity 27 */ 28 private void grow(int minCapacity) 29 // overflow-conscious code 30 int oldCapacity = elementData.length; 31 int newCapacity = oldCapacity + (oldCapacity >> 1); //右移操作,相当于oldCapacity/2 扩容之后为原来的1.5倍,也称之为扩容系数 32 if (newCapacity - minCapacity < 0) // 新容量时否小于最小需要的容量,小于则新容量为最小需要容量 33 newCapacity = minCapacity; 34 if (newCapacity - MAX_ARRAY_SIZE > 0) //新容量比最大数组容量还大,则需要判断 35 newCapacity = hugeCapacity(minCapacity); 36 // minCapacity is usually close to size, so this is a win: 37 elementData = Arrays.copyOf(elementData, newCapacity); 38 39 40 private static int hugeCapacity(int minCapacity) 41 if (minCapacity < 0) // overflow 42 throw new OutOfMemoryError(); 43 return (minCapacity > MAX_ARRAY_SIZE) ? 44 Integer.MAX_VALUE : 45 MAX_ARRAY_SIZE; 46
我们分析一下该扩容机制:
(1)当我们add第一个元素时,此时数组初始化(无参构成时)的还是
DEFAULTCAPACITY_EMPTY_ELEMENTDATA =
则此时
minCapacity = DEFAULT_CAPACITY //10
因此
ensureExplicitCapacity(int minCapacity)方法中 10 - 0 > 0的,所以会进行第一次扩容,进入到grow(int minCapacity)方法之后,先获取到oldCapacity为0,newCapacity通
过位移运行也为0,则newCapacity=minCapacity=10;容量确定为10扩容操作copyOf。
(2)当第二次add元素操作时,minCapacity 为2,此时elementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不
会进入 grow(minCapacity) 方法。所以就不会扩容,直至到添加第11个元素时,minCapacity(11) > elementData.length(10),进入grow方法进行扩容。进入grow方法之后,
newCapacity变为10 + 10 >> 1 = 15,此时15大于最小需要容量,则将数组扩容为15。以此类推下去进行扩容.....
四、ArrayList的add()方法
众所周知,ArrayList适合查询操作,问为什么?肯定都会说效果高(因为它支持下标随机访问),那添加操作呢?效率不高吗?那我们带着这些疑问来一起看看源码。
1 /** 2 * Appends the specified element to the end of this list. 3 * 4 * @param e element to be appended to this list 5 * @return <tt>true</tt> (as specified by @link Collection#add) 6 */ 7 public boolean add(E e) 8 ensureCapacityInternal(size + 1); // Increments modCount!! 9 elementData[size++] = e; 10 return true; 11
1 /** 2 * Inserts the specified element at the specified position in this 3 * list. Shifts the element currently at that position (if any) and 4 * any subsequent elements to the right (adds one to their indices). 5 * 6 * @param index index at which the specified element is to be inserted 7 * @param element element to be inserted 8 * @throws IndexOutOfBoundsException @inheritDoc 9 */ 10 public void add(int index, E element) 11 rangeCheckForAdd(index); 12 13 ensureCapacityInternal(size + 1); // Increments modCount!! 14 System.arraycopy(elementData, index, elementData, index + 1, 15 size - index); 16 elementData[index] = element; 17 size++; 18
这里我们只看这两种添加,另外的两种addAll思想和这里的第二种方法类似,就不再做过多的介绍。不难看出二者的区别:
(1)、add(E e) 做的操作第一步先进行缓存容量的计算,第二步直接给当前维护的内部缓存数组下一个数据位置填充当前添加的数据。从逻辑结构来看,它是直接在数组的末尾添加了元素,从物理结构来考虑,是直接在当前数据存储的物理地址最后面进行开辟了一块空间进行连续存储,同时给当前维护数组大小的变量size++操作。
(2)、add(int index, E element) 操作就有趣的多,它是在当前下标为index的位置添加element,然后将原来index位置的元素进行后移(并不会将原来index位置元素覆盖掉),先不看源码,我们可以想到数组后移,必定会需要先扩容,然后再进行移动数据。那么问题来了,如果现在有5000个元素,我要在index = 2 的位置插入一个元素,那要移动4998位,可想而知,在磁盘上进行移动操作,和直接在末尾追加操作哪个效率高就不用我多说了。现在我们看看源码时如何实现的,首先去检查插入的index位是不是越界,然后再计算缓存容量,达到缓存界限及时扩容(物理地址上的扩容)。然后做一个arraycopy的动作,这个动作用于将当前维护的数组size变为size+1的大小,同时进行移动数据操作。最后进行index位置赋值位element。
综上,ArrayList的add操作一定效率低吗?答案当然是不一定,如果是在ArrayList的末尾添加元素,效率当然高。另外的两种addAll同样源码会对其先toArray操作,再arraycopy操作。
五、ArrayList的remove()方法
在这里,肯定有很多同学疑惑,在工作中写代码时发现写了一个for循环去删除某个条件的值时,发现会偶尔报错,有时成功,有时失败报异常。为何会出现这种情况呢?那我们接下来就看看它的源码是如何操作,看完之后或许你会恍然大悟。ArrayList作为容器,肯定会为我们提供容器的基本操作方法。remove就是为我们提供的其中一个。
(1)根据下标删除指定元素:
1 /** 2 * Removes the element at the specified position in this list. 3 * Shifts any subsequent elements to the left (subtracts one from their 4 * indices). 5 * 6 * @param index the index of the element to be removed 7 * @return the element that was removed from the list 8 * @throws IndexOutOfBoundsException @inheritDoc 9 */ 10 public E remove(int index) 11 rangeCheck(index); 12 13 modCount++; 14 E oldValue = elementData(index); 15 16 int numMoved = size - index - 1; 17 if (numMoved > 0) 18 System.arraycopy(elementData, index+1, elementData, index, 19 numMoved); 20 elementData[--size] = null; // clear to let GC do its work 21 22 return oldValue; 23
我们可以看出,源码所做的事情是先获取到index位置的元素,然后进行arraycopy操作,进行将数组元素前移(底层使用for循环),最关键的一点是最后一句
elementData[--size] = null;
这一行做了一个操作,将数组的size进行减1操作,这才是为什么有的同学在使用for循环删除时会发生异常的根本原因。
例如:ArrayList中的值为[1,3,2,5,2],现在要删除为2的,第一次找到2时下标为2,remove(2)不会发生异常,然而此时list的大小已经从5变成了4,而循环的大小size还是5,导致循环下标index为4的时候会发生异常。
(2)同样,删除指定元素objec也是同理:
1 /** 2 * Removes the first occurrence of the specified element from this list, 3 * if it is present. If the list does not contain the element, it is 4 * unchanged. More formally, removes the element with the lowest index 5 * <tt>i</tt> such that 6 * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt> 7 * (if such an element exists). Returns <tt>true</tt> if this list 8 * contained the specified element (or equivalently, if this list 9 * changed as a result of the call). 10 * 11 * @param o element to be removed from this list, if present 12 * @return <tt>true</tt> if this list contained the specified element 13 */ 14 public boolean remove(Object o) 15 if (o == null) 16 for (int index = 0; index < size; index++) 17 if (elementData[index] == null) 18 fastRemove(index); 19 return true; 20 21 else 22 for (int index = 0; index < size; index++) 23 if (o.equals(elementData[index])) 24 fastRemove(index); 25 return true; 26 27 28 return false; 29
指定删除元素比直接删除指定下标更为复杂一点,内部需要先去for循环遍历找到该元素的index,再根据index去删除,fastRemove(index)方法也是通过arraycopy形式进行移动元素,之后进行size减1操作。源码:
1 /* 2 * Private remove method that skips bounds checking and does not 3 * return the value removed. 4 */ 5 private void fastRemove(int index) 6 modCount++; 7 int numMoved = size - index - 1; 8 if (numMoved > 0) 9 System.arraycopy(elementData, index+1, elementData, index, 10 numMoved); 11 elementData[--size] = null; // clear to let GC do its work 12
因此,ArrayList中的remove方法可能会带来一些隐患,使用时需要注意,同时效率也是不言而喻的。如果非要使用remove并且还不会发生异常,那我们又该怎么办呢?请看下面第五节。
六、ArrayList的Iterator
ArrayList的内部维护了内部类Iterator,每一个容器都会有自己的迭代器,迭代器作用是为了可以快速的去轮询ArrayList容器,API建议我们去使用迭代器轮询元素,而不是使用for循环,迭代器为我们提供了规范的接口。
1 /** 2 * Returns @code true if the iteration has more elements. 3 * (In other words, returns @code true if @link #next would 4 * return an element rather than throwing an exception.) 5 * 6 * @return @code true if the iteration has more elements 7 */ 8 boolean hasNext(); 9 10 /** 11 * Returns the next element in the iteration. 12 * 13 * @return the next element in the iteration 14 * @throws NoSuchElementException if the iteration has no more elements 15 */ 16 E next(); 17 18 /** 19 * Removes from the underlying collection the last element returned 20 * by this iterator (optional operation). This method can be called 21 * only once per call to @link #next. The behavior of an iterator 22 * is unspecified if the underlying collection is modified while the 23 * iteration is in progress in any way other than by calling this 24 * method. 25 * 26 * @implSpec 27 * The default implementation throws an instance of 28 * @link UnsupportedOperationException and performs no other action. 29 * 30 * @throws UnsupportedOperationException if the @code remove 31 * operation is not supported by this iterator 32 * 33 * @throws IllegalStateException if the @code next method has not 34 * yet been called, or the @code remove method has already 35 * been called after the last call to the @code next 36 * method 37 */ 38 default void remove() 39 throw new UnsupportedOperationException("remove"); 40
ArrayList的内部类去实现了该接口,先看源码:
1 /** 2 * An optimized version of AbstractList.Itr 3 */ 4 private class Itr implements Iterator<E> 5 int cursor; // index of next element to return 6 int lastRet = -1; // index of last element returned; -1 if no such 7 int expectedModCount = modCount; 8 9 Itr() 10 11 public boolean hasNext() 12 return cursor != size; 13 14 15 @SuppressWarnings("unchecked") 16 public E next() 17 checkForComodification(); 18 int i = cursor; 19 if (i >= size) 20 throw new NoSuchElementException(); 21 Object[] elementData = ArrayList.this.elementData; 22 if (i >= elementData.length) 23 throw new ConcurrentModificationException(); 24 cursor = i + 1; 25 return (E) elementData[lastRet = i]; 26 27 28 public void remove() 29 if (lastRet < 0) 30 throw new IllegalStateException(); 31 checkForComodification(); 32 33 try 34 ArrayList.this.remove(lastRet); 35 cursor = lastRet; 36 lastRet = -1; 37 expectedModCount = modCount; 38 catch (IndexOutOfBoundsException ex) 39 throw new ConcurrentModificationException(); 40 41 42
(1)hasNext()方法,size为ArrayList内容维护数组实际大小的值,cursor游标为当前访问的位置,如果游标位置刚好指到数组最后一个位置时则为false,此时停止轮询遍历。
(2)next()方法,游标从0开始,每次+1,同时记录最后一次访问的下标,直接从数组中获取该游标减1(这里的i)的值。
(3)remove方法,先调用ArrayList容器提供的remove方法,上面已经看过remove方法,此时size会减1,同时此时迭代器的下标已经到了该删除元素的下一个位置,所以为了防止下标越界,所以将游标位置前移一位,cursor的下一次访问从删除元素位置开始。这样就保证了删除时不会发生异常。
七、ArrayList的set()方法
该方法给某个下标为修改为设置的值,不用过多解释,直接看源码。
1 /** 2 * Replaces the element at the specified position in this list with 3 * the specified element. 4 * 5 * @param index index of the element to replace 6 * @param element element to be stored at the specified position 7 * @return the element previously at the specified position 8 * @throws IndexOutOfBoundsException @inheritDoc 9 */ 10 public E set(int index, E element) 11 rangeCheck(index); 12 13 E oldValue = elementData(index); 14 elementData[index] = element; 15 return oldValue; 16
八、ArrayList的get()方法
这里也不做过多解释,直接看源码就明白。
1 /** 2 * Returns the element at the specified position in this list. 3 * 4 * @param index index of the element to return 5 * @return the element at the specified position in this list 6 * @throws IndexOutOfBoundsException @inheritDoc 7 */ 8 public E get(int index) 9 rangeCheck(index); 10 11 return elementData(index); 12
九、总结
ArrayList作为容器,加上前面已经分析过它的源码,则可以确定出:
(1)在ArrayList的尾部插入效率高,随机访问效率高(内部是一个数组)。
(2)但是如果要在中间插入或者删除则效率低(需要对数组里面的节点进行位移操作arraycopy非常耗时)。
(3)使用for循环时不要轻易使用remove方法。
(4)遍历时推荐使用迭代器,不推荐使用for循环。
线性表之顺序存储结构(顺序表)(代码片段)
目录1.线性表2.顺序存储结构(顺序表)2.1顺序表一般可以分为: 1.静态顺序表:使用定长数组存储元素 1.1静态顺序表:顺序表的大小是确定的,容量是固定的. 结论:静态顺序表不是很常量,了解就行.2.动态顺序表:... 查看详情
数据结构学习总结线性表之顺序表(代码片段)
通过前面的学习知道,具有“一对一”逻辑关系的数据,最佳的存储方式是使用线性表。那么,什么是线性表呢?线性表,全名为线性存储结构。使用线性表存储数据的方式可以这样理解,即“把所有数据用一... 查看详情
线性表之顺序表(代码片段)
线性表的结构体定义:#defineMaxSize100typedefstructintdata[MaxSize];intlength;Sqlist; 顺序表在内存中以数组形式保存,是一组连续的内存空间。顺序表基本算法:构造一个空的线性表://初始化线性表STATUSInitList(Sqlist&L)//置表长为0L.len... 查看详情
数据结构线性表之实现单链表(代码片段)
数据结构线性表之实现单链表数据结构线性表之实现单链表相关文章单链表定义单链表相关操作数据结构线性表之实现单链表相关文章数据结构线性表之概念(一)数据结构线性表之抽象基类(二)数据结构线性表之实现顺序表(三)数... 查看详情
线性表之栈(代码片段)
栈的定义: 一种只能在一端进行插入或删除操作的线性表被称为栈,其中允许删除或插入的一端为栈顶,另一端为栈底,栈底固定不变; 栈的特点:先进后出,例如弹夹,先装的子弹最后才能出; 按照存储结构可以... 查看详情
02线性表之顺序表(代码片段)
肚子饿了就要吃 ~ 嗝 ———路飞 目录1.本章重点2.线性表3.顺序表实现3.1顺序表的概念及结构4.静态顺序表5.动态顺序表 5.1“增”尾插:头插:5.2“删”尾删头删5.3随机“插入”5.4随机“删除”5.5内存销毁5.6“改”5... 查看详情
数据结构之线性表之顺序存储java实现(代码片段)
文章目录线性表的定义线性表的基本运算线性表的存储之顺序存储定义线性表添加元素查找元素删除元素打印线性表实现的完整代码测试一下线性表的定义线性表的逻辑特征:①有且仅有一个称为开始元素的a1,她没有前... 查看详情
数据结构线性表之实现单循环链表(代码片段)
数据结构线性表之实现单循环链表数据结构线性表之实现单循环链表相关文章单循环链表单循环链表定义数据结构线性表之实现单循环链表相关文章数据结构线性表之概念(一)数据结构线性表之抽象基类(二)数据结构线性表之实... 查看详情
线性表之单链表(代码片段)
在学完线性表之后,总结一下顺序表的优缺点优点无须为元素之间的逻辑结构增添额外的储存空间,自成一体。随机存取,十分方便。缺点空间利用率不高,容易造成“碎片”。插入删除操作需要移动大量的元素。当线性... 查看详情
03线性表之链表(代码片段)
肚子饿了就要吃 ~ 嗝 ———路飞 1.本章重点链表表示和实现(单链表+双链表)链表的常见OJ题顺序表和链表的区别和联系2.为什么需要链表引子:顺序表的问题及思考(1)动态顺序表特点:插入数据,... 查看详情
数据结构——线性表之顺序表篇(代码片段)
目录前言顺序表的介绍和简单实现关于顺序表顺序表的分类顺序表的初步组成功能函数的声明功能函数的实现初始化检查容量判断是否需要扩容头插头删 尾插尾删指定数据位置插入指定数据删除寻找数据并返回下标销毁顺序表... 查看详情
数据结构学习总结线性表之单链表(代码片段)
一,回忆链表 链表,别名链式存储结构或单链表,用于存储逻辑关系为"一对一"的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。&nbs... 查看详情
数据结构线性表循环链表之约瑟夫环(代码片段)
(一)前提41个人报数,1-3,当谁报数为3,谁就去嗝屁。现在获取他们嗝屁的顺序(二)实现结构顺序:3->1->5->2->4 (三)代码实现#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<stdlib.h>#include<time.h>#defineOK1#... 查看详情
线性表之链表(代码片段)
单链表也是一种链式存取的线性表,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,以next指针指向下一个节点而链接起来,相比于顺序表,链表有着快速增加,删除节点的优势,其节点... 查看详情
线性表之顺序存储-顺序表(代码片段)
顺序表的操作[x]向有序顺序表插入一个元素[x]顺序表的冒泡排序[x]顺序表的删除操作[x]顺序表中元素的查找[x]顺序表的逆置[x]删除顺序表中的相同元素[x]向顺序表的指定位置插入元素[x]打印顺序表顺序表的存储结构#definemaxsize100/... 查看详情
数据结构与算法-线性表之循环链表(代码片段)
前言:前面几篇介绍了线性表的顺序和链式存储结构,其中链式存储结构为单向链表(即一个方向的有限长度、不循环的链表),对于单链表,由于每个节点只存储了向后的指针,到了尾部标识就停止了向后链的操作。也就是说... 查看详情
线性链表之顺序表
...,具有这一特点的存储结构称为[随机存储]。使用的基本数据结构:数组特点:顺序存取,随机访问。/* Name:SeqList Copyright:1.0 Author:JohnnyZen Date:04/06/1721:51 Description:线性链表之顺序表*// 查看详情
数据结构(线性表之单链表)(代码片段)
文章目录🥇为何要使用链表🥇单链表是什么单链表的数据存储方式🥇单链表之创建链表🥇单链表之打印链表🥇单链表之计算链表长度🥇单链表之增删查改单链表之头插法单链表之尾插法单链表之把一个... 查看详情