什么是动态数组?
在编程中,我们经常用到数组这种最基本的数据结构。而我们一般用的都是定长数组,动态数组用的会比较少。但是有时,定长数组并不能很好地满足我们的要求,于是我们就要使用用动态数组。
在JAVA中,已经有一个封装好的API——ArrayList,它就是一个动态数组我们可以直接拿过来使用。但是有时我们可能需要自己定制一个动态数组,以便更好地解决我们的问题。所以,我们就来用JAVA实现一个动态数组。实现动态数组的方法有两种,一种是用定长数组实现(顺序表),另一种是用引用(链表)实现。在这里,我们将要讲的是定长数组实现动态数组的方法。
实现的思路:
首先先申请一个数组空间data,定义一个int型的数据size,记录当前已使用的空间大小,也就是动态数组的长度。然后在每次增加元素的操作中对当前动态数组的长度和已开辟的空间大小进行比较。如果当前的数组空间不足(size==data.length),那么我们就新开辟一个大小为原来的数组长度2倍的新数组,先把原来数组的信息赋值到这个新的数组中,再增加新的数据。
那么,可以对数组进行扩容,同样也可以对数组进行缩小,即就是当对数组中的元素删除后,通过判断data.length>DEFAULT_SIZE&&size<=data.length/4,对数组进行缩容。如此我们就实现了动态数组。
缩容操作
动态数组的常用操作如下:
/**
* List是线性表的最终父接口
* */
public interface List<E> {
/**
* 获取线性表中元素的个数(线性表的长度)
* @return 线性表中有效元素的个数
* */
public int getSize();
/**
* 判断线性表是否为空
* @return 是否为空的布尔类型值
* */
public boolean isEmpty();
/**
* 在线性表中指定的index角标处添加元素e
* @param index 指定的角标 0<=index<=size
* @param e 要插入的元素
* */
public void add(int index,E e);
/**
* 在线性表的表头位置插入一个元素
* @param e 要插入的元素 指定在角标0处
* */
public void addFirst(E e);
/**
* 在线性表的表尾位置插入一个元素
* @param e 要插入的元素 指定在角标size处
* */
public void addLast(E e);
/**
* 在线性表中获取指定index角标处的元素
* @param index 指定的角标 0<=index<size
* @return 该角标所对应的元素
* */
public E get(int index);
/**
* 获取线性表中表头的元素
* @return 表头元素 index=0
* */
public E getFirst();
/**
* 获取线性表中表尾的元素
* @return 表尾的元素 index=size-1
* */
public E getLast();
/**
* 修改线性表中指定index处的元素为新元素e
* @param index 指定的角标
* @param e 新元素
* */
public void set(int index,E e);
/**
* 判断线性表中是否包含指定元素e 默认从前往后找
* @param e 要判断是否存在的元素
* @return 元素的存在性布尔类型值
* */
public boolean contains(E e);
/**
* 在线性表中获取指定元素e的角标 默认从前往后找
* @param e 要查询的数据
* @return 数据在线性表中的角标
* */
public int find(E e);
/**
* 在线性表中删除指定角标处的元素 并返回
* @param index 指定的角标 0<=index<size
* @return 删除掉的老元素
* */
public E remove(int index);
/**
* 删除线性表中的表头元素
* @return 表头元素
* */
public E removeFirst();
/**
* 删除线性表中的表尾元素
* @return 表尾元素
* */
public E removeLast();
/**
* 在线性表中删除指定元素e
* */
public void removeElement(E e);
/**
* 清空线性表
* */
public void clear();
}
具体代码实现:
public class ArrayList<E> implements List<E> {
private static int DEFAULT_SIZE = 10; //容器的默认容量
private E[] data;//存储数据元素的容器
private int size;//线性表的有效元素的个数
//data.length表示线性表的最大容量capacity
/**
* 创建一个容量默认为10的一个线性表
*/
public ArrayList() {
this(DEFAULT_SIZE);
}
/**
* 创建一个容量为指定capacity的一个线性表
* @param capacity
*/
public ArrayList(int capacity) {
this.data = (E[]) new Object[capacity];
this.size = 0;
}
/**
* 将一个数组封装成为一个线性表
* @param arr
*/
public ArrayList(E[] arr) {
data=(E[]) new Object[arr.length];
for(int i=0;i<data.length;i++) {
data[i]=arr[i];
}
size=data.length;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(int index, E e) {
if(index<0||index>size) {
throw new ArrayIndexOutOfBoundsException("add函数角标越界");
}
//判断是否已满
if(size==data.length) {
resize(2*data.length);
}
for(int i=size-1;i>=index;i--) {
data[i+1]=data[i];
}
data[index]=e;
size++;
}
/**
* 改变data数组的长度(扩,缩)
* @param newlen
*/
private void resize(int newLen) {
E[] newData=(E[]) new Object[newLen];
for(int i=0;i<size;i++) {
newData[i]=data[i];
}
data=newData;
}
@Override
public void addFirst(E e) {
add(0,e);
}
@Override
public void addLast(E e) {
add(size,e);
}
@Override
public E get(int index) {
if(index<0 || index>size-1) {
throw new ArrayIndexOutOfBoundsException("get函数角标越界");
}
return data[index];
}
@Override
public E getFirst() {
// TODO 自动生成的方法存根
return get(0);
}
@Override
public E getLast() {
// TODO 自动生成的方法存根
return null;
}
@Override
public void set(int index, E e) {
if(index<0 || index>size-1) {
throw new ArrayIndexOutOfBoundsException("set函数角标越界");
}
data[index]=e;
}
@Override
public boolean contains(E e) {
if(isEmpty()) {
return false;
}
for(int i=0;i<size;i++) {
if(data[i]==e) {
return true;
}
}
return false;
}
@Override
public int find(E e) {
if(isEmpty()) {
return -1;
}
for(int i=0;i<size;i++) {
if(data[i]==e) {
return i;
}
}
return -1;
}
@Override
public E remove(int index) {
if(index<0||index>size-1){
throw new ArrayIndexOutOfBoundsException("remove函数角标越界");
}
E e=get(index);
for(int i=index+1;i<=size-1;i++){
data[i-1]=data[i];
}
size--;
//判断是否缩容 1.最短不能缩过默认容量 2.有效元素的个数小于等于容量的1/4
if(data.length>DEFAULT_SIZE&&size<=data.length/4){
resize(data.length/2);
}
return e;
}
@Override
public E removeFirst() {
return remove(0);
}
@Override
public E removeLast() {
return remove(size-1);
}
@Override
public void removeElement(E e) {
int index=find(e);
if(index==-1) {
throw new IllegalArgumentException("删除元素不存在");
}
remove(index);
}
@Override
public void clear() {
size=0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ArrayList: size="+size+",capacity="+data.length+"\n");
if(isEmpty()) {
sb.append("[]");
}else {
sb.append('[');
for(int i=0;i<size;i++) {
sb.append(data[i]);
if(i==size-1) {
sb.append(']');
}
else {
sb.append(',');
}
}
}
return sb.toString();
}
public int getCapacity() {
return data.length;
}
}
注意:因为数组中只能存储某一种数据类型,不能掺杂其他数据;但是,有时我们又希望要求数组中可以存储任何数据类型。要满足这两个看似有些矛盾的要求就只能使用Java的泛型。泛型不是引用类型也不是基本数据类型;泛型是一种特殊的符号,它泛指Java中任何一种引用类型。泛型起到一个占位符的作用,之后在使用的过程中可以根据具体需求来指定数据类型。
总结:
这里我们采用的是双倍扩展数组空间,我认为是为了降低算法的空间复杂度。如果采用每增加一个元素增加一个数组空间的话,每次添加函数都要进行一次赋值操作,该操作的时间复杂度是n,一旦n值较大,就会降低算法的效率。