【数据结构】线性表 ( 线性表概念简介 | 顺序存储结构 / 链式存储结构 | 顺序存储结构 - 顺序表 List | 顺序表 ArrayList 源码分析 )

2023-10-11 16:48:47 浏览数 (1)

一、线性表概念简介

线性表 是 一组 按照顺序排列 的元素 组成的 数据集合 ;

线性表有两种存储结构 :

  • 顺序存储结构 : 在内存中存储的数据是连续的 , 如 : 数组 ;
  • 链式存储结构 : 在内存中存储的数据是不连续的 , 如 : 链表 ;

线性表 中

  • 除第一个元素外 , 每个元素都有一个 唯一的前驱元素 ;
  • 除最后一个元素外 , 每个元素都有一个 唯一的后继元素 ;

所有的元素 形成了一条线性的结构。

二、顺序存储结构 - 顺序表 List

顺序存储结构 就是 顺序表 List ;

顺序存储结构:

  • 内存连续 : 顺序存储结构 在 内存中 使用连续的内存空间 来存储线性表中的元素。
  • 索引访问 : 在顺序存储结构中,数据元素 按照特定顺序 依次存放在 内存中的连续地址空间中,可以通过索引来访问元素。索引就是内存地址 ;
  • 顺序存储结构 ( 顺序表 ) 示例 :
    • 数组
    • ArrayList , 其内部也是数组实现的 ;

顺序表 优点:

  • 随机访问: 通过 索引下标 可以 直接访问 内存中 指定位置的元素 , 时间复杂度为 O(1) ;
  • 存储效率高: 不需消耗额外空间定义指针指向其它元素存,只需要元素本身的存储空间。

顺序表 缺点:

  • 插入和删除效率低: 顺序存储结构 中,插入 和 删除 操作 需要整体移动所有元素 ,时间复杂度为 O(n) ;
  • 固定存储空间: 数组在创建时需要指定固定的大小,创建后该大小不可改变 ;

顺序表代码示例 : 顺序表直接存储在数组中 ;

代码语言:javascript复制
class Students {
	Student[20];
	int size;
}

三、顺序表 ArrayList 源码分析

在 Java 中的 ArrayList 就是 顺序表 , 下面分析其源码 ;

ArrayList 源代码地址 : https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/ojluni/src/main/java/java/util/ArrayList.java

代码语言:javascript复制
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认初始容量。
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于空实例的共享空数组实例。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小的空实例的共享空数组实例。我们
     * 将其与EMPTY_ELEMENTDATA区分开来,以了解何时膨胀多少
     * 添加第一个元素。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存储ArrayList元素的数组缓冲区。
     * ArrayList的容量就是这个数组缓冲区的长度。任何
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * 将在添加第一个元素时扩展为DEFAULT_CAPACITY。
     */
    transient Object[] elementData; // 非私有以简化嵌套类访问

    /**
     * 数组列表的大小(包含的元素数量)。
     *
     * @serial
     */
    private int size;
}

默认的 ArrayList 的 初始容量为 int DEFAULT_CAPACITY = 10 , 如果调用不含任何参数的构造函数 , 则默认的初始容量就是 10 ;

代码语言:javascript复制
    /**
     * 构造一个初始容量为10的空列表。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

在 ArrayList # add 方法中 , 调用了 ensureCapacityInternal 函数 处理数组的大小是否够用 ;

调用 elementData[size ] = e 代码 , 向数组末尾添加元素 e ;

代码语言:javascript复制
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size   1);  // Increments modCount!!
        elementData[size  ] = e;
        return true;
    }

在 ensureCapacityInternal 函数中 , 调用了 ensureExplicitCapacity 函数 ,

代码语言:javascript复制
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

在 ensureExplicitCapacity 函数中 , 调用了 grow 函数 , 就是增加 ArrayList 容量的函数 ;

代码语言:javascript复制
    private void ensureExplicitCapacity(int minCapacity) {
        modCount  ;

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

在 grow 函数中 , 创建了新的数组 , 并将原来数组中的数据拷贝到新数组中 ;

代码语言:javascript复制
    /**
     * 增加容量以确保它至少可以容纳最小容量参数指定的元素数量。
     *
     * @param minCapacity 所需的最小容量
     */
    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);
    }

上面的 最小容量 是当前大小 1 ;

如果要 向指定位置插入元素 , 要先将 当前数组 前 index 个元素进行拷贝 , 然后拷贝要插入的元素 , 最后将 原数组中 index 后的元素进行拷贝 ;

其中涉及到了 两次拷贝 , 操作的过程很烦碎 ;

代码语言:javascript复制
    /**
     * 将指定元素插入此列表中的指定位置。
     * 将当前在该位置的元素(如果有的话)和任何后续元素向右移动(在它们的索引上加1)。
     *
     * @param index 要插入指定元素的索引
     * @param element 要插入的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size   1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index   1,
                         size - index);
        elementData[index] = element;
        size  ;
    }

下面代码的作用是 将 elementData 源数组 , 从 index 位置开始的元素 , 拷贝到 elementData 目的数组 的 index 1 位置 , 拷贝 size - index 个元素 , 相当于将 elementData 数组从 index 开始的元素都向后挪动了一个位置 ;

代码语言:javascript复制
        System.arraycopy(elementData, index, elementData, index   1,
                         size - index);

System.arraycopy 函数原型 :

代码语言:javascript复制
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

调用 ArrayList # remove 函数 , 删除某个元素 , 是将 index 后的元素都向前移动一个位置 ;

代码语言:javascript复制
    public E remove(int index) {
        rangeCheck(index);

        modCount  ;
        E oldValue = elementData(index);

        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

        return oldValue;
    }

下面代码的作用是 将 elementData 源数组 , 从 index 1 位置开始的元素 , 拷贝到 elementData 目的数组 的 index 位置 , 拷贝 numMoved 个元素 , 相当于将 elementData 数组从 index 开始的元素都向前挪动了一个位置 ;

代码语言:javascript复制
            System.arraycopy(elementData, index 1, elementData, index,
                             numMoved);

0 人点赞