动态数组
数组是一种顺序储存的线性表,所有元素的内存地址是连续.
1 2
| int[] array = new int[]{20, 30, 40}
|
![在这里插入图片描述]()
动态数组
创建ArrayList类,创建size属性来管理数组中元素的个数,创建element属性来管理存取的数据.
可以对动态数组进行增删改查操作.
![]()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class ArrayList<E> { private int size; private E[] elements;
int size(); boolean isEmpty(); boolean contains(E element); void add(E element); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(E element); void clear(); }
|
动态数组的实现
构造方法
如果构建的数组空间小于默认空间,则会以默认空间创建数组.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class ArrayList<E> {
private int size;
private E[] elements;
private static final int DEFAULT_CAPACITY = 10;
private static final int ELEMENT_NOT_FOUNT = -1;
public ArrayList() { this(DEFAULT_CAPACITY); }
public ArrayList(int capacity) { capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity]; } }
|
添加元素
- 数组添加元素分为
在最后一个元素的后面添加新元素
和将元素插入到某个位置(非最后面)
两种情况。
- 第一种情况,这个
新元素需要添加到的索引
等于当前数组元素的个数
,在ArrayList中size
属性就是当前数组元素的个数
,所以就是将新元素添加到数组的size
位置上,然后size加1
。
![]()
1 2 3 4
| public void add(int index, E element) { elements[index] = element; size++; }
|
- 如果是第二种情况,只需要将插入位置后面的元素向后移动即可。
- 注意:需要从后向前移动元素,如果从前向后移动元素,那么会进行元素覆盖, 最后出错。
![]()
数组越界
添加元素,还要注意传入的索引不能越界,即不能小于0,也不能大于size.
1 2 3 4 5 6 7 8 9 10
|
private void rangeCheckForAdd(int index) { if (index < 0 || index > size) { indexOutOfBounds(index); } }
|
1 2 3 4 5 6 7 8 9
|
private void indexOutOfBounds(int index) { throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size); }
|
数组扩容
动态扩容思路:
通过默认容量创建的数组,是在堆空间中随机生成的地址;如此一来再申请空间拼接到该数组后,这种方式不可能实现;
我们只能再创建一个大容量的数组,然后将之前数组中的元素移动到这个数组中;然后将引用指向新数组即可!
由于数组elements最大的容量只有10,所以当数组存满元素时,就需要对数组进行扩容。
因为数组是无法动态扩容的,所以需要创建一个新的数组,这个数组的容量要比之前数组的容量大。
然后在将原数组中的元素存放到新数组中,这样就实现了数组的扩容。
![]()
- 该方法确保默认容量为多少,为了验证
是否超过给定的默认容量
,然后进行判断是否要扩容;这里size+1
为数组当前数量+1, 因为每次add
都会增加一个容量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length; if (oldCapacity >= capacity) return;
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity]; for (int i = 0; i < size; i++) { newElements[i] = elements[i]; } elements = newElements; }
|
实现add函数,需要在添加新元素之前,判断数组越界和扩容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public void add(int index, E element) { rangeCheckForAdd(index);
ensureCapacity(size + 1);
for (int i = size - 1; i >= index; i--) { elements[i + 1] = elements[i]; } elements[index] = element; size++; }
|
最终在最后一个元素的后面添加新元素,即添加元素到尾部的实现方式如下
1 2 3 4 5 6 7 8 9 10
|
public void add(E element) { add(size, element); }
|
删除元素
- 删除元素,实际上就是
移除指定位置的元素
,并将后面的元素向前移动
。
- 如下图,当删除索引为
3
的元素时,只需要将后面的元素向前移动,然后在去掉最后一个元素,size减1
即可。
![]()
数组越界
- 删除元素时传入的索引不能越界, 即不能小于0, 也不能大于等于size所以我们在删除元素之前需要先进行索引检查。
1 2 3 4 5 6 7 8 9
| private void indexOutOfBounds(int index) { throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size); } private void rangeCheck(int index) { if (index < 0 || index >= size) { outOfBounds(index); } }
|
数组缩容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public void trim() { int capacity = elements.length; if (size >= capacity >> 1 || capacity < CAPACITY_DEFAULT) return; int newCapacity = capacity >> 1; E[] newElements = (E[]) new Object[newCapacity]; for (int i = 0; i < size; i++) { newElements[i] = elements[i]; } elements = newElements; }
|
最终, remove方法实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public E remove(int index) { rangeCheck(index); E delEle = elements[index];
for (int i = index + 1; i <= size - 1; i++) { elements[i - 1] = elements[i]; } size--; elements[size] = null; trim(); return delEle; }
public void remove(E element){ remove(indexOf(element)); }
|
清空数组
清空数组时,需要将所有的元素置为null,只有这样才能真正的释放对象,然后size置为0。
![]()
1 2 3 4 5 6 7 8 9
|
public void clear() { for (int i = 0; i < size; i++) { elements[i] = null; } size = 0; }
|
修改元素
修改元素时,只需要将原有位置的元素替换
掉即可,同样需要注意一下索引是否越界
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public E set(int index, E element) { rangeCheck(index);
E oldEle = elements[index]; elements[index] = element; return oldEle; }
|
查询元素
查询元素,只需要将指定索引的元素返回,注意索引是否越界即可。
1 2 3 4 5 6 7 8 9 10 11
|
public E get(int index) { rangeCheck(index); return elements[index]; }
|
查看元素位置
- 可以通过循环, 查找元素在数组中的位置。
- 注意:假如数组中可以存储
null
,而null是不能调用equals
方法的,所以需要对传入的元素进行判断,如果查找的元素是null
,需要单独处理。
- 当元素存在时返回索引,否则返回变量
ELEMENT_ON_FOUND
的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public int indexOf(E element) { if (element == null) { for (int i = 0; i < size; i++) { if (elements[i] == null) return i; } } else { for (int i = 0; i < size; i++) { if (element.equals(elements[i])) return i; } } return ELEMENT_NOT_FOUNT; }
|
是否包含某元素
只需通过判断索引是否等于ELEMENT_ON_FOUND
即可。
1 2 3 4 5 6 7 8 9 10
|
public boolean contains(E element) { return indexOf(element) != ELEMENT_NOT_FOUNT; }
|
元素的数量
size的值,即为元素的数量
1 2 3 4 5 6 7 8 9
|
public int size() { return size; }
|
数组是否为空
通过判断size
的值是否为0
即可
1 2 3 4 5 6 7 8 9
|
public boolean inEmpty() { return size == 0; }
|
动态数组打印
可以重写toString
方法, 来打印ArrayList
中的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Override public String toString() { StringBuilder string = new StringBuilder(); string.append("size=").append(size).append(", ["); for (int i = 0; i < size; i++) { string.append(elements[i]); if (i != size - 1) { string.append(", "); } } string.append("]"); return string.toString(); }
|
这样就实现了动态数组的基本操作。
ArrayList能否进一步优化?
- 在ArrayList中,如果要删除索引0位置的元素,则需要将索引0之后的元素全部往前移一位。
- 如果要在索引
0
位置添加元素,也需要将索引0
及之后的元素全部往后移一位
。
![]()
- 在ArrayList中
增加一个记录首元素位置的属性
。
- 删除索引
0
位置的元素,我们只需要将first
属性改为1
。
- -在索引
0
位置添加元素,则只需要将first
属性改为0
。
![]()
- 如果继续往前插入元素,则将插入的元素存放在索引
8
这个位置,并将first
改为8
。
- 当要获取索引
8
下一个元素时,对索引取模
,则拿到索引0
位置的元素。
![]()
- 如果插入元素,则可选择挪动
前半段
数据或后半段
数据。
- 在索引
2
处插入元素99
,可以选择将元素22
,33
左移,然后插入99
即可。
扩容
和缩容
同样可以优化。
![]()
重点总结
动态扩容思路
通过默认容量创建的数组,是在堆空间中随机生成的地址;如此一来
再申请空间拼接到该数组后,这种方式不可能实现;
我们只能再创建一个大容量的数组,然后将之前数组中的元素移动到
这个数组中;然后将引用指向新数组即可!
如何确保容量是否越界
该方法确保默认容量为多少, 为了验证是否超过给定的默认容量,然后进行判断是否要扩容; 这里size+1为数组当前数量+1, 因为每次add都会增加一个容量。
1
| ensureCapacity(size + 1);
|
增加泛型
使用泛型, 使动态数组可以添加任何类型的数据。
1
| elements = (E[]) new Object[capacity];
|
clear方法的过渡细节
因为之前存储的都是int数据, 直接设置size=0时, 开辟的存储int类型的空间就不能被访问, 当add后, 就可以访问后面的空间, 所以此时的空间可以重复利用;
当使用泛型后, 动态数组中存储的都是对象类型, 实际存储的都是对象的地址, 每一个对象的地址又有一块空间引用着; 此时如果仍设置 size=0, 当clear后,开辟的存储空间中的地址没有被销毁, 地址仍被对象空间引用着; 这样以来存储对象的空间就不会被释放; 但是存储地址的数组可以重复利用; 所以要将地址数组都置为null, 然后size=0, 这样以来,引用该地址的对象空间也就释放了!
remove、indexOf的细节
remove最后一个地址也要情况, 同clear细节
在indexOf方法中,不用==比较, 因为比较的是地址值,一般重写equals方法自己定义比较内容即可;
null值处理: 当往数组传null的时候,indexOf的比较处理: 如果那null.equals来比较会出现空指针异常;