数据结构之顺序表与Java中的ArrayList、Vector

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_45608306/article/details/100610124

顺序表

1.什么是顺序表

顺序表是线性表的一种。
线性表的定义为:一种可以在任意位置进行插入和删除数据元素操作的,由n个相同类型数据元素 a 0 , a 1 , a 2 , , a n 1 a_{0},a_{1},a_{2},\cdots,a_{n-1} 组成的线性结构。其数据元素满足除第一个元素和最后一个元素外,每个元素只有一个直接前驱和直接后继,第一个元素没有前驱元素,最后一个元素没有后继元素。
顺序表通常基于数组来实现,数组将顺序表的数据元素存储在一块连续地址空间的内存单元中。这样逻辑上相邻的元素在物理存储地址上也相邻,数据元素见逻辑上的前驱后继关系表现在数据元素存储单元的前后位置关系上。

2.顺序表的存储结构

顺序表存储结构
其中myArray为构造顺序表所依托的数组结构,maxSize为数组长度,也即顺序表的最大表长,而size为顺序表的实际表长。

3.顺序表类成员以及方法

通常对于一个数据结构最基本的操作即:插入,删除,修改,查找。 因而将顺序表成员及方法定义如下:

int maxSize;         //表最大长度
int size;         //表实际元素个数
Object[] MyArray;      //表依托的数组

public int size();         //求表中元素个数
public boolean isEmpty();    //判断表是否为空
public void insert();       //插入元素
public Objcet delete(int i) throws Exception;   //删除元素
public void setData(int i) throws Exception;    //修改元素
public Object getData(int i) throws Exception;   //查找元素

4.顺序表基本功能实现

public class myArrayList{
    private static final int defaultSize = 10;  //默认长度
    private int maxSize;
    private int size;
    private Object[] myArray;
    public myArrayList(){
        maxSize = defaultSize;
        size = 0;
        myArray = new Object[defaultSize];
    }

    public myArrayList(int size){
        maxSize = size;
        this.size = 0;
        myArray = new Object[size];
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void insert(int i, Object obj) throws Exception{
        if (size == maxSize){
            throw new Exception("full");
        }
        if (i<0||i>size){
            throw new Exception("parameter error");
        }
        for (int j = size;j > i;j--){   //插入位置后面的元素,从末尾开始依次往后移一个单位
            myArray[j] = myArray[j-1];
        }
        myArray[i] = obj;
        size++;
    }

    public Object delete(int i) throws Exception{
        if (size == 0){
            throw new Exception("empty");
        }
        if (i<0||i>size-1){
            throw new Exception("parameter error");
        }
        Object data = myArray[i];
        for (int j=i;j<size-1;j++){    //删除位置后面的元素,从前面开始依次往前移动一个单位
            myArray[j] = myArray[j+1];
        }
        size--;
        return data;
    }

    public void setData(int i, Object obj) throws Exception{
        if (i<0||i>size){
            throw new Exception("parameter error");
        }
        myArray[i] = obj;
    }

    public Object getData(int i) throws Exception{
        if (i<0||i>size){
            throw new Exception("parameter error");
        }
        return myArray[i];
    }

}

5.性能分析

5.1 插入
插入位置为size时,插入操作需要往后移动0个元素;
插入位置为size-1时,插入操作需要往后移动1个元素;
插入位置为size-1时,插入操作需要往后移动2个元素;
依此类推可得,插入操作平均移动元素为:
1 s i z e + 1 k = 0 s i z e ( s i z e k ) = s i z e 2 \frac{1}{size+1}\sum_{k=0}^{size}{(size-k)}=\frac{size}{2}
故插入操作时间复杂度为 O ( N ) O(N)
5.3 删除:
同理可得删除操作平均移动元素为:
1 s i z e k = 0 ( s i z e 1 ) = s i z e 1 2 \frac{1}{size}\sum_{k=0}{(size-1)}=\frac{size-1}{2}
故删除操作时间复杂度为 O ( N ) O(N)
5.4 修改
由于顺序表可以随机访问,故修改操作时间复杂度为 O ( 1 ) O(1)
5.5 查询
由于顺序表可以随机访问,故查询操作时间复杂度为 O ( 1 ) O(1)

ArrayList

1.ArrayList简述

实际上,Java中的ArrayList类就是数据结构顺序表的一种实现。
ArrayList继承自AbstractList,实现了List接口,提供线性表的基本功能,增删改查以及一些基于这些的拓展功能。
实现了RandomAccess接口。事实上,这个接口没有需要实现的方法,它实际上是一个标记,表示这种集合可随机访问。
实现了Cloneable接口,提供了拷贝功能,即clone方法。实现了Serializable接口,提供了序列化功能,能够序列化传输该类对象。

2.ArrayList部分源码

2.1 部分成员

    private static final int DEFAULT_CAPACITY = 10;   //默认容量
    private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; //便于构造器快速创建新数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];  //同上
    transient Object[] elementData;  //依托的数组元素
    private int size;    //实际大小
    private static final int MAX_ARRAY_SIZE = 2147483639;   //最大容量

	protected int modCount;    //继承自AbstractList的域,统计列表结构修改的次数。主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构。

2.2 构造方法
三个构造方法,前两个与之前的顺序表基本一致(抛出的异常类型不同);第三个构造方法根据提供的列表对象类型创建对应的对象数组。

    public ArrayList(int var1) {
        if (var1 > 0) {
            this.elementData = new Object[var1];
        } else {
            if (var1 != 0) {
                throw new IllegalArgumentException("Illegal Capacity: " + var1);
            }
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> var1) {
        this.elementData = var1.toArray();
        if ((this.size = this.elementData.length) != 0) {
            if (this.elementData.getClass() != Object[].class) {
                this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
            }
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

2.3 插入
同样,首先是越界检查,之后判断数组容量是否足够,不足则将数组扩容一个单位;之后插入位置后面的所有元素后移,最后插入所给元素。

    public boolean add(E var1) {
        ++this.modCount;
        this.add(var1, this.elementData, this.size);  //调用另一个add方法
        return true;
    }

    public void add(int var1, E var2) {
        this.rangeCheckForAdd(var1);   //越界检查
        ++this.modCount;
        int var3;
        Object[] var4;
        if ((var3 = this.size) == (var4 = this.elementData).length) { //判断容量是否足够,不够则扩增
            var4 = this.grow();   //扩容
        }
        System.arraycopy(var4, var1, var4, var1 + 1, var3 - var1); //自身复制一部分,即类同上述顺序表的移动元素,效果是将数组中自var1开始的var3-var1长度的部分向后移动一格
        var4[var1] = var2;
        this.size = var3 + 1;
    }

2.4 删除
与顺序表操作稍有不同。首先检查越界,之后判断删除元素位置,若非末尾元素则元素前移,最后末位置空。

    public E remove(int var1) {
        Objects.checkIndex(var1, this.size);  //越界检查
        Object[] var2 = this.elementData;
        Object var3 = var2[var1];
        this.fastRemove(var2, var1);  //从列表中删除某元素的函数
        return var3;
    }
    private void fastRemove(Object[] var1, int var2) {
        ++this.modCount;
        int var3;
        if ((var3 = this.size - 1) > var2) {
            System.arraycopy(var1, var2 + 1, var1, var2, var3 - var2);  //元素前移
        }
        var1[this.size = var3] = null;  //置空
    }

2.5 修改
类似顺序表给出的操作,检查越界后修改元素。

    public E set(int var1, E var2) {
        Objects.checkIndex(var1, this.size);
        Object var3 = this.elementData(var1);
        this.elementData[var1] = var2;
        return var3;
    }

2.6 查询

扫描二维码关注公众号,回复: 7578203 查看本文章
    public E get(int var1) {
        Objects.checkIndex(var1, this.size);
        return this.elementData(var1);
    }
	E elementData(int var1) {
        return this.elementData[var1];
    }

3.ArrayList扩容机制

3.1 扩容扩的是什么
扩容扩的是对象内部维护的Object[] elementData数组(size加粗样式是数组中实际存在(非null)的元素),通过增大这个数组,在添加操作,保证了在索引没有问题(0<index<size)的情况下不会抛出越界异常。
3.2 容量扩大了多少
先抛结论:默认扩为原来容量的1.5倍,也可手动进行控制。
插入元素时会调用grow()方法进行自动扩容,继而调用grow(int x)方法,根据newCapacity()返回的值改变数组容量。然而,若多次插入操作中,需要频繁自动扩容,数组的频繁复制带来较大的开销,为避免这个问题此时可通过ensureCapacity(int x)方法来实现手动扩容。
源码

    public void ensureCapacity(int var1) {
        if (var1 > this.elementData.length && (this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA || var1 > 10)) {
            ++this.modCount;
            this.grow(var1);
        }
    }

    private Object[] grow(int var1) {   //提供期望值,根据实际返回值新建并拷贝数组
        return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(var1));
    }

    private Object[] grow() {   //插入操作时调用的方法
        return this.grow(this.size + 1);
    }

    private int newCapacity(int var1) {   //根据期望容量返回新容量数值
        int var2 = this.elementData.length;  //获取原数组最大长度
        int var3 = var2 + (var2 >> 1);   //原数组最大长度的1.5倍
        if (var3 - var1 <= 0) {   //若1.5倍小于期望容量
            if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //若为默认容量
                return Math.max(10, var1);   //取默认容量和期望容量最大的那个
            } else if (var1 < 0) {    //检查数值越界
                throw new OutOfMemoryError();
            } else {
                return var1;
            }
        } else {  //取原容量1.5倍与最大容量更小的那个
            return var3 - 2147483639 <= 0 ? var3 : hugeCapacity(var1);
        }
    }

    private static int hugeCapacity(int var0) {
        if (var0 < 0) {
            throw new OutOfMemoryError();
        } else {
            return var0 > 2147483639 ? 2147483647 : 2147483639;
        }
    }

3.3 可否缩容
可使用trimToSize()方法将表调整实际大小。

4.其他

4.1 该类的iterator和listIterator方法返回的迭代器是"快速失败"的:如果在创建迭代器之后的任何时候使用了迭代器自身的remove或add方法之外的任何方法对列表进行结构修改,迭代器将抛出ConcurrentModificationException。

Vector

1.Vector与ArrayList类似,,是一种顺序表,区别在于它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
2.Vector的自动扩容机制与ArratList不同。它有一个capacityIncrement域,即扩容时递增的大小,在构造时可初始化此域。若此域大于0,扩容时将扩容这个数值,否则扩容为两倍。

总结

1.由于ArrayList和Vector等顺序表是通过数组来实现,因而它们支持理论上的随机访问。
2.ArrayList和Vector等顺序表插入和删除操作较为耗时(相比于链表来说),若进行频繁的插入与删除操作,建议使用LinkList。而顺序表查找和修改操作时间复杂度为 O ( 1 ) O(1) ,故查找与修改是ArratList的长处。
3.ArrayList是线程不安全的容器,在需要线程同步访问时避免使用它;而Vector是线程安全的容器,故需要线程同步时使用它,而线程同步带来了巨大的开销,故在不平常情况下建议使用ArrayList。
4.ArrayList扩容机制通常为1.5倍;Vector扩容机制通常为一个固定数值,若固定数值为0,则扩容为2倍。

猜你喜欢

转载自blog.csdn.net/qq_45608306/article/details/100610124