数组结构与算法之动态数组

前言

前文我们提到过数组的一些缺点,包括无法扩容,封装性较差等,我们可以通过自定义一个动态数组来解决一下这些问题。
在java中早已有成熟的ArrayList可以供我们日常使用,或者线程安全的Vector等;C++中也有Vector。这是语言提高给我们已经封装好的动态数组。
但是,我们仍然应当了解其基本原理,动态数组其实不是一种很复杂的数据结构,它更像是数组的优化版本。
以下,是动态数组的java实现:

泛型支持

动态数组,作为被封装的工具类,应当是支持泛型的。

Java泛型(generics)是JDK 5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许程序员在编译时监测非法的类型。使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。

我们这个动态数组,就取英译名DynamicArray:

/**
 * @program: thinking-in-all
 * @description:
 * @author: Lucifinil
 * @create: 2019-12-26
 **/
public class DynamicArray<T> {
}
属性

这里的动态数组是一个教为简单的实现,首先,因为它本质上是对数组的封装与扩展,其第一个属性为泛型数组:data,其次,动态数组支持动态扩容,其容量值为第二个属性:capacity;最后,我们应当有一个当前大小,来确定是否扩容:size,即:

    //被封装的数组
    private T[] data;
    //当前动态数组的元素数量
    private int size;
    //当前动态数组的容量
    private int capacity;
初始化

对于动态数组的初始化而言,支持直接传入capacity构造器,传入一个数组的构造器,或者无参构造,因为在动态数组初始化时,size始终应当为0,且data应当被初始化:
在java中,泛型数组无法通过这样的写法来初始化:

传入capacity的构造器

 data = new T[capacity];

正确的写法为通过泛型数组强转Object数组:

    public DynamicArray(int capacity) {
        this.capacity = capacity;
        data = (T[])new Object[capacity];
        size=0;
    }

传入数组的构造器

    public DynamicArray(T[] arr) {
        this.data =  (T[]) (new Object[arr.length]);
        for (int i = 0; i < arr.length; i++) {
            data[i] = arr[i];
        }
        size = arr.length;
        capacity=size+1;
    }

无参构造器
参考ArrayList,设置初始容量为10:

public DynamicArray() {
        this(10)
辅助函数

获取动态数组当前元素数量:

    public int size(){
        return size;
    }

获取动态数组当前是否非空:

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

获取动态数组当前容量大小:

    public int capacity(){
        return capacity;
    }
元素的添加与扩容

在这里插入图片描述

上图,黑色代表原本存在的元素,灰色表示未使用的元素(int数组默认值为0),绿色表示新增值,红色表示迁移后的值。

扩容函数:

    private void grow(int capacity) {
        this.capacity = capacity;
        T[] newData = (T[])new Object[capacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

当data中插入一个元素后,如果其位置之后存在元素,也就是图中的3,4它会相应向后迁移;
插入元素前会进行检查,一旦data的容量被全部使用,data的大小会扩充为原来的1.5倍:

    public void add(int index, T e) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Add failed.Index is invalid.");
        }
        //如果当前元素数量已经等于容量值,扩容为原数组大小的1.5倍
        if (size == capacity) {
            grow(capacity + (capacity >> 1));
        }
        //对数据进行迁移
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        //赋值
        data[index] = e;
        //添加元素后 size应当加1
        size++;
    }
元素的删除与缩容

前文提到了当动态数组的实际使用已经到达容量值的时候,动态数组会进行扩容操作,那么,当动态数组的使用远远到达不了容量值的时候,动态数组应该进行缩容吗?
比如前文的数组,进行删除5的操作后:
在这里插入图片描述

上述图片中的自动缩容是与自动扩容相对的,也就是说一旦size等于capacity就会扩容,而一旦size等于capacity的2/3便会缩容,这会产生一个临界问题,那就是,如果我们经常在旧的扩容前的capacity大小的索引左右增删数据,那么动态数组便会不断地扩容缩容,如下图:
举个例子:

其实,除了这种自动缩容外,还有主动缩容的策略,如ArrayList中的trimToSize():

    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

它是用户主动调用,来将其底层数组的大小缩容为当前size大小。

我们,这里选择自动扩容的优化版本,只有当size为capacity的一半时,才进行缩容,这样便不会产生临界震荡问题,其实,各种策略都可以,在不同的场景下各有优劣。

删除元素,且在删除元素后检查来决定是否缩容:

    public T remove(int index){
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Remove failed.Index is invalid.");
        }
        T oldValue = data[index];
        for (int i = index+1; i < size ; i++) {
            data[i - 1] = data[i];
        }
        size--;
        data[size] = null;
        //size为容量值的一半,且容量值的一半不为0,进行缩容
        if (size == this.capacity>>1 && this.capacity>>1!=0) {
            grow(this.capacity>>1);
        }
        return oldValue;
    }
元素的改查

其实,元素的修改与查询是直接对底层数组的修改与查询,由于工具类中data是私有的,故而应当暴露相应接口:
修改:

    public void set(int index, T e) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Set failed.Index is invalid.");
        }
        data[index] = e;
    }

查询:

    public T get(int index) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Get failed.Index is invalid.");
        }
        return data[index];
    }
完整代码
/**
 * @program: thinking-in-all
 * @description:
 * @author: Lucifinil
 * @create: 2019-12-26
 **/
public class DynamicArray<T> {
    //数组
    private T[] data;
    //当前动态数组的元素数量
    private int size;
    //当前动态数组的容量
    private int capacity;
    public DynamicArray(int capacity) {
        this.capacity = capacity;
        data = (T[]) new Object[capacity];
        size = 0;
    }
    public DynamicArray(T[] arr) {
        this.data = (T[]) (new Object[arr.length]);
        for (int i = 0; i < arr.length; i++) {
            data[i] = arr[i];
        }
        size = arr.length;

        capacity = size + 1;
    }
    public DynamicArray() {
        this(10);
    }
    public int size() {
        return size;
    }
    public boolean isEmpty() {
        return size == 0;
    }
    public int capacity() {
        return capacity;
    }
    //判断动态数组中是否包含e
    public boolean contains(T e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return true;
            }
        }
        return false;
    }
    public void add(int index, T e) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Add failed.Index is invalid.");
        }
        //如果当前元素数量已经等于容量值,扩容为原数组大小的1.5倍
        if (size == capacity) {
            grow(capacity + (capacity >> 1));
        }
        //对数据进行迁移
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        //赋值
        data[index] = e;
        //添加元素后 size应当加1
        size++;
    }
    //给动态数组尾部添加元素
    public void addLast(T e) {
        add(size, e);
    }
    private void grow(int capacity) {
        this.capacity = capacity;
        T[] newData = (T[]) new Object[capacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }
    public T remove(int index) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Remove failed.Index is invalid.");
        }
        T oldValue = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        size--;
        data[size] = null;
        //size为容量值的一半,且容量值的一半不为0,进行缩容
        if (size == this.capacity >> 1 && this.capacity >> 1 != 0) {
            grow(this.capacity >> 1);
        }
        return oldValue;
    }
    public T removeLast() {
        return remove(size - 1);
    }
    public void set(int index, T e) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Set failed.Index is invalid.");
        }
        data[index] = e;
    }
    //给动态数组尾部修改元素
    public void setLast(T e) {
        set(size - 1, e);
    }
    public T get(int index) {
        //如果索引值非法,抛出索引越界异常
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Get failed.Index is invalid.");
        }
        return data[index];
    }
    //获取尾部元素
    public T getLast() {
        return get(size - 1);
    }
    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        str.append("[");
        for (int i = 0; i < size; i++) {
            str.append(data[i]);
            if (i != size - 1) {
                str.append(",");
            }
        }
        str.append("]");
        return str.toString();
    }
}

总结

这个工具类很多东西是没有考虑周全的,比如java中集合类都应implement的Iterable接口里的方法等等,一般来说,动态数组只需要包含上述基本方法就行了,理解到动态数组的思想才是最为重要的。
我是路西菲尔,如有错误,敬请指正,期待与你一同成长!
转载请注明出处,来自路西菲尔的博客https://blog.youkuaiyun.com/csdn_1364491554/article/details/103699256

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值