数据结构--顺序表与ArratList

本文深入探讨了ArrayList的核心思想,分析了插入、删除、修改和查找等基本操作的实现原理。ArrayList通过数组存储数据,插入和删除操作可能涉及元素移动,效率相对较低,尤其是当需要扩容时。而查找操作由于数组的特性,具有O(1)的时间复杂度。文章还详细解析了扩容策略,即每次扩容为原来的1.5倍。
摘要由CSDN通过智能技术生成

今天是1024,写一篇文章纪念下。文章主要学习顺序表的核心思想,关于ArrayList不会分析每个方法的实现,也没必要,把握核心思想即可。

一、什么是顺序表

顺序表是线性表的一种,是一种顺序存储结构。顺序表存储数据时,会提前申请一整块足够大的物理空间,然后将数据依次存储起来,存储时做到数据元素之间不留缝隙。比如我们常见的数组就属于顺序表。
在这里插入图片描述

二、顺序表的基本操作

  • 插入:由于顺序表是一种顺序存储结构,所有存储的元素在物理内存上是连续的,如果我们需要往顺序表中插入一个元素,就需要将要插入位置元素以及后续的元素整体向后移动一个位置,在插入之前还需要判断申请的空间是否足够,如果不够还需要扩容。然后再将要插入的元素放到腾出来的位置上。
  • 删除:从顺序表中删除指定元素,只需找到目标元素的位置,并将其后续所有元素整体前移 1 个位置即可。
  • 修改:直接通过下标找到该元素,重新赋值即可。
  • 查找:由于顺序表所存储的元素在屋里内存上是连续的,因此可以直接通过下标访问该元素。比如访问a[10],a就是数组的头指针,a[10]就代表a指针所指位置向后偏移10个a类型大小的空间,这样就指向了下标为10所代表的元素了,因此顺序表的访问效率非常高。

三、ArratList源码分析

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //ArratList中真正保存数据的数组
    transient Object[] elementData; // non-private to simplify nested class access

    //ArratList中元素的数量
    private int size;

可以看到ArratList内部是通过数组(elementData)来保存元素的。
当我们调用无参构造的时候,默认使用一个空的数组。

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

当构造ArrayList时如果指定了大小,创建一个初始大小(initialCapacity)的数组。

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

添加元素分析:

当调用add(E e) 方法回望尾部添加一个元素。

 public boolean add(E e) {
        //确保数组的大小足够,如果不够会进行一次扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //往数组elementData最后一个元素之后再添加一个元素
        elementData[size++] = e;
        return true;
    }

那么 ensureCapacityInternal(size + 1); 方法是如何进行扩容的呢?

private void ensureCapacityInternal(int minCapacity) {
        //1
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //2
        ensureExplicitCapacity(minCapacity);
    }

首先看注释1:如果是通过无参的构造创建的ArrayList,然后第一次执行 add(E e)方法那么elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA成立,minCapacity 会被赋值为DEFAULT_CAPACITY(10)与入参minCapacity(1)之间的最大值,也就值10。否则的话minCapacity大小为已有的元素的size+1,然后执行ensureExplicitCapacity(minCapacity)

 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

ensureExplicitCapacity这个方法很简单,就是判断是否需要扩充数组长度,如果minCapacity 大于数组的长度,就会走到 grow(minCapacity)去扩容,否则就代表不需要扩充数组大小,此时该方法啥也不干。

那么grow又是如何扩容的呢?

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

easy!先获取数组原本的长度oldCapacity,然后乘以1.5就是新的长度newCapacity 。
如果newCapacity小于minCapacity,那么就将其赋值给newCapacity ,最后调用Arrays.copyOf将elementData扩容到newCapacity长度大小。
Arrays.copyOf扩容的方式如下:

  public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

首先创建一个新的长度的数组,然后将原本数组中的数据拷贝到新的数组中。这样就完成了一次扩容的过程。

add(E e)是往集合的尾部添加元素,除了这个方法ArrayList还可以往集合中指定的位置添加元素add(int index, E element)

  public void add(int index, E element) {
        //首先进行边界检查,index不能超出插入的范围
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
		//判断是否需要扩容,如果需要就先扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //从需要插入的位置开始所有的元素整体后移一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //在空出来的index位置添加上新的元素
        elementData[index] = element;
        size++;
    }

通过分析add方法可以看到Arraylist对数据的添加是比较消耗性能的,如果是尾部添加还好,在不需要扩容的情况下直接在最后一个元素之后追加一个新元素就行了,但如果在集合中某个指定的位置添加元素,就会导致集合中对应下标元素包括其后的元素整体后移一下,然后再在空出来的位置添加元素,是比较消耗性能的。

删除元素分析

 public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];
       //计算出要删除的元素之后还有多少个元素
        int numMoved = size - index - 1;
        if (numMoved > 0)
        //将要删除的元素之后的所有元素整体往前移一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //因为最后一个元素已经前移了,所以此时可以将该位置的引用置null,以便GC回收
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

需要注意只是将最后一个元素置空,但没有缩减数组的长度。

除了remove(int index)通过位置删除方法,还有remove(Object o)删除指定的对象的方法

 public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
        //便利所有的对象,找到第一个和o相等的元素,index就是所在下标,然后通过fastRemove(index)删除
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

fastRemove(int index)方法的逻辑和remove(int index)的逻辑一样就不重复分析了。

  private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

修改元素分析

  public E set(int index, E element) {
        //边界检查
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        E oldValue = (E) elementData[index];
        //将指定位置的元素从新赋予新值,这样就完成修改了。
        elementData[index] = element;
        return oldValue;
    }

查找元素分析

  public E get(int index) {
        //边界检查
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
    }

前面说过,顺序表存储数据时,会提前申请一整块足够大的物理空间,然后将数据依次存储起来。正是由于这中存储方式,在查找元素的时候时候的迅速,直接通过索引(指针)就可以访问到指定位置的元素。

ArrayList总结

  • ArrayList 访问元素的时间复杂度是多少?为什么?
    ArrayList 底层是数组存储,数组在内存中是连续的地址空间,随机访问效率很高O(1)。

  • ArrayList的大小是如何自动增加的?
    如果第一次调用无参构造创建的ArrayList,数组的大小是0,当调用了add会扩充到10。之后当数组所需的大小大于现有的大小的时候,就会进行扩容,新的容量为旧的容量的1.5倍。

  • ArrayList的增加或者删除某个对象效率很低吗?解释一下为什么?
    如果是在尾部添加,并且数组现有的容量足够(不需要扩容)效率还是可以的,如果需要扩容或者在某个指定位置添加就会涉及System.arraycopy效率不是很好,尤其在数据中有大量的数据的情况下。如果是删除尾部的元素,效率还是可以的,如果是其他指定位置的元素,删除操作也会涉及System.arraycopy效率不是很好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值