动态数组
什么是数据结构?
数据结构是计算机存储、组织数据的方式
在实际应用中,根据使用场景来选择最合适的数据结构
参考:数据结构与算法动态可视化
线性表
线性表是具有 n 个相同类型元素的有限序列( n ≥ 0 )
- a1 是首节点(首元素), an 是尾结点(尾元素)
- a1 是 a2 的前驱, a2 是 a1 的后继
常见的线性结构有
- 数组
- 链表
- 栈
- 队列
- 哈希表(散列表)
数组(Array)
数组是一种顺序存储的线性表, 所有元素的内存地址都是连续的
int[] array = new int[]{11, 22, 33}
在很多编程语言中,数组有个致命的缺点,无法动态修改容量。
实际开发中我们希望数组的容量是动态变化的。
动态数组(Dynamic Array)接口设计
int size();//元素的数量
boolean isEmpty();//是否为空
boolean contains(E element);//是否包含某个元素
void add(E element);//添加元素到最后面
E get(int index);//返回index位置对应的元素
E set(int index, E element);//设置index位置的元素
void add(int index, E element);//往index位置添加元素
E remove(int index);//删除index位置对应的元素
int indexOf(E element);//查看元素的位置
void clear();//清除所有元素
动态数组的设计
在Java中,成员变量会自动初始化,比如
- int 类型自动初始化为0
- 对象(object) 类型自动初始化为null
创建类ArrayList
创建size属性来管理数组中元素的个数, 创建elements属性来管理存取的数据
public class ArrayList<E> {
/**
* 元素数量
*/
private int size;
/**
* 所有的元素 数组
*/
private E[] elements;
}
添加初始化方法
创建elements数组, 并指定elements默认的容量
public class ArrayList<E> {
/**
* 元素数量
*/
private int size;
/**
* 所有的元素 数组
*/
private E[] elements;
// 设置elements数组默认的初始化空间
private static final int DEFAULT_CAPACITY = 10;
/**
* 可以自定义数组容量
* @param capacity 容量
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity];
}
/**
* 无参构造 默认情况10
*/
public ArrayList() {
this(DEFAULT_CAPACITY);
}
}
完善接口中容易的方法
实现元素的数量、是否为空、返回index位置对应的元素、查看元素的位置的方法,后续会进行优化
public class ArrayList<E> {
/**
* 元素数量
*/
private int size;
/**
* 所有的元素 数组
*/
private E[] elements;
// 设置elements数组默认的初始化空间
private static final int DEFAULT_CAPACITY = 10;
// 表示元素不存在
private static final int ELEMENT_NOT_FOUND = -1;
/**
* 可以自定义数组容量
* @param capacity 容量
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity];
}
/**
* 无参构造 默认情况10
*/
public ArrayList() {
this(DEFAULT_CAPACITY);
}
/**
* 元素的数量
* @return
*/
public int size(){
return size;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty(){
return size == 0;
}
/**
* 是否包含某个元素
* @param element 元素
* @return
*/
public boolean contains(E element){
return indexOf(element) != ELEMENT_NOT_FOUND;
}
/**
* 返回index位置对应的元素
* @param index
* @return
*/
public E get(int index){
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
}
return elements[index];
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return
*/
public E set(int index, E element){
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
}
E old = elements[index];
elements[index] = element;
return old;
}
/**
* 查看元素的位置
* @param element
* @return
*/
public int indexOf(E element){
for (int i = 0; i < size; i++) {
if (elements[i] == element) return i;
}
return ELEMENT_NOT_FOUND;
}
/**
* 清除所有元素
*/
public void clear(){
size = 0;
}
}
添加元素 - add(E element)
数组添加元素分为在最后一个元素的后面添加新元素和将元素插入到某个位置(非最后面)两种情况。
- 第一种情况,这个新元素需要添加到的索引等于当前数组元素的个数,在ArrayList中size属性就是当前数组元素的个数,所以就是将新元素添加到数组的size位置上,然后size加1。
public class ArrayList<E> {
......
/**
* 添加元素到最后面
* @param element
*/
public void add(E element){
elements[size++] = element;
}
......
}
打印元素 - toString()
- 重写toString方法
- 在toString 方法中将元素拼接成字符串
- 字符串拼接建议使用StringBuilder
public class ArrayList<E> {
......
/**
* 打印
* @return
*/
@Override
public String toString() {
//Size = ? , [1, 2, 3]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(elements[i]);
// 还多了减法运算
// if (i!=size - 1){
// string.append(", ");
// }
}
string.append("]");
return string.toString();
}
......
}
删除元素 - remove(int index)
- 删除元素, 实际上就是去掉指定位置的元素,并将后面的元素向前移动
- 当删除索引为3的元素时,只需要将后面的元素向前移动,然后在去掉最后一个元素,size减1
- 注意: 删除元素时传入的索引不能越界, 即不能小于0, 也不能大于等于size
- 所以我们在删除元素之前需要先进行索引检查
public class ArrayList<E> {
......
/**
* 删除index位置对应的元素
* @param index
* @return
*/
public E remove(int index){
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
}
E old = elements[index];
for (int i = index + 1; i <= size - 1; i++) {
elements[i -1] = elements[i];
}
size -- ;
return old;
}
......
}
添加元素 - add(int index, E element)
添加第二种情况,只需要将插入位置后面的元素向后移动。
- 注意:需要从后向前移动元素,如果从前向后移动元素,那么会进行元素覆盖, 最后出错
public class ArrayList<E> {
......
/**
* 往index位置添加元素
* @param index
* @param element
*/
public void add(int index, E element){
if (index < 0 || index > size){
throw new IndexOutOfBoundsException("Index:"+index+",Size:"+size);
}
for (int i = size - 1; i >= index; i--){
elements[i + 1] = elements[i];
}
elements[index] = element;
size++;
}
......
}
代码优化
将上面的越界异常提示以及边界判断封装,优化部分已标注
public class ArrayList<E> {
/**
* 元素数量
*/
private int size;
/**
* 所有的元素 数组
*/
private E[] elements;
// 设置elements数组默认的初始化空间
private static final int DEFAULT_CAPACITY = 10;
// 表示元素不存在
private static final int ELEMENT_NOT_FOUND = -1;
/**
* 可以自定义数组容量
* @param capacity 容量
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity];
}
/**
* 无参构造默认 10
*/
public ArrayList() {
this(DEFAULT_CAPACITY);
}
/**
* 元素的数量
* @return
*/
public int size(){
return size;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty(){
return size == 0;
}
/**
* 是否包含某个元素
* @param element 元素
* @return
*/
public boolean contains(E element){
return indexOf(element) != ELEMENT_NOT_FOUND;
}
/**
* 添加元素到最后面 --- 优化
* @param element
*/
public void add(E element){
// elements[size++] = element;
add(size,element);
}
/**
* 返回index位置对应的元素 --- 优化
* @param index
* @return
*/
public E get(int index){
rangeCheck(index);
return elements[index];
}
/**
* 设置index位置的元素 --- 优化
* @param index
* @param element
* @return
*/
public E set(int index, E element){
rangeCheck(index);
E old = elements[index];
elements[index] = element;
return old;
}
/**
* 往index位置添加元素 --- 优化
* @param index
* @param element
*/
public void add(int index, E element){
rangeCheckForAdd(index);
for (int i = size - 1; i >= index; i--){
elements[i + 1] = elements[i];
}
elements[index] = element;
size++;
}
/**
* 删除index位置对应的元素 --- 优化
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
E old = elements[index];
for (int i = index + 1; i <= size - 1; i++) {
elements[i -1] = elements[i];
}
size -- ;
return old;
}
/**
* 查看元素的位置
* @param element
* @return
*/
public int indexOf(E element){
for (int i = 0; i < size; i++) {
if (elements[i] == element) return i;
}
return ELEMENT_NOT_FOUND;
}
/**
* 清除所有元素
*/
public void clear(){
size = 0;
}
/**
* 打印
* @return
*/
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(elements[i]);
}
string.append("]");
return string.toString();
}
/**
* 越界异常提示 --- 优化
* @param index
*/
private void outOfBounds(int index){
throw new IndexOutOfBoundsException("Index:"+index+", Size:"+size);
}
/**
* 边界判断 --- 优化
* @param index
*/
private void rangeCheck(int index){
if (index < 0 || index >= size){
this.outOfBounds(index);
}
}
/**
* 添加时边界判断 --- 优化
* @param index
*/
private void rangeCheckForAdd(int index){
if (index < 0 || index > size){
this.outOfBounds(index);
}
}
}
数组扩容 - 添加元素容量不足时触发
- 由于数组elements最大的容量只有10, 所以当数组存满元素时, 就需要对数组进行扩容
- 因为数组是无法动态扩容的, 所以需要创建一个新的数组,这个数组的容量要比之前数组的容量大
- 然后在将原数组中的元素存放到新数组中, 这样就实现了数组的扩容
public class ArrayList<E> {
......
/**
* 往index位置添加元素 --- 优化
* @param index
* @param element
*/
public void add(int index, E element){
rangeCheckForAdd(index);
//扩容
//当前size=10 如果再添加至少需要11个 (size + 1 是保证至少的容量)
ensureCapacity(size + 1);
for (int i = size - 1; i >= index; i--){
elements[i + 1] = elements[i];
}
elements[index] = element;
size++;
}
/**
* 容量扩容自定义的1.5倍: 扩容因子1.5
* 保证要有capacity的容量
* @param capacity
*/
private void ensureCapacity(int capacity){
int oldCapacity = elements.length;
//没有超过默认或者当前数组容量
if (oldCapacity >= capacity) return;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E [] newElements = (E[])new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
......
}
对象数组
- 清空数组时,需要将所有的元素置为null,只有这样才能真正的释放对象,然后size置为0。
- 内存管理的细节,防止造成内存垃圾
public class ArrayList<E> {
......
/**
* 删除index位置对应的元素 --- 优化
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
E old = elements[index];
for (int i = index + 1; i <= size - 1; i++) {
elements[i -1] = elements[i];
}
// size -- ;
elements[--size] = null;
return old;
}
/**
* 清除所有元素 --- 优化
* 并不是真正的清除 只是将数组元素数量置为0,其实在数组中还是存在原来的数据,只不过访问不到而已
* 对对象进行优化 将元素置null
*/
public void clear(){
for (int i=0;i<size;i++){
elements[i] = null;//让每个对象为null
}
size = 0;
}
......
}
使用equals 代替 ==
== 比较的是对象内存地址是不是相同
equals 比较变量或者实例指向同一个内存空间的值是不是相同
public class ArrayList<E> {
......
/**
* 查看元素的位置 --- 优化
* @param element
* @return
*/
public int indexOf(E element){
for (int i = 0; i < size; i++) {
// if (elements[i] == element) return i;
if (elements[i].equals(element)) return i;
}
return ELEMENT_NOT_FOUND;
}
......
}
null的处理
在添加时,默认是可以添加null元素,但是在获取元素的索引会报异常
public class ArrayList<E> {
......
/**
* 查看元素的位置 --- 优化
* @param element
* @return
*/
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 (elements[i] == element) return i;
// if (elements[i].equals(element)) return i;
if (element.equals(elements[i])) return i;
}
}
return ELEMENT_NOT_FOUND;
}
......
}
代码优化
public class ArrayList<E> {
......
/**
* 往index位置添加元素 --- 优化
* @param index
* @param element
*/
public void add(int index, E element){
rangeCheckForAdd(index);
//扩容
//当前size=10 如果再添加至少需要11个 (size + 1 是保证至少的容量)
ensureCapacity(size+1);
for (int i = size ; i > index; i--){
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
/**
* 删除index位置对应的元素 --- 优化
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
E old = elements[index];
for (int i = index + 1; i < size; i++) {
elements[i -1] = elements[i];
}
// size -- ;
elements[--size] = null;
return old;
}
/**
* 根据元素刪除
* @param element
*/
public void remove(E element){
remove(indexOf(element));
}
......
}
数组缩容 - 删除元素容量过大时触发
- 当数组中的元素删除后,数组剩余的空间可能会很大,这样就会造成内存的浪费。
- 所以当数组中元素删除后,我们需要对数组进行缩容。
- 实现方法类似于扩容,当数组中容量小于某个值时,创建新的数组,然后将原有数组中的元素存入新数组。
public class ArrayList<E> {
......
/**
* 删除index位置对应的元素 --- 优化
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
E old = elements[index];
for (int i = index + 1; i < size; i++) {
elements[i -1] = elements[i];
}
// size -- ;
elements[--size] = null;
//缩容 当前的0.5 但小于等于默认 容量后不再缩
trim();
return old;
}
/**
* 清除所有元素 --- 优化
* 并不是真正的清除 只是将数组元素数量置为0,其实在数组中还是存在原来的数据 ,只不过访问不到而已
* 对对象进行优化
*/
public void clear(){
for (int i=0;i<size;i++){
elements[i] = null;//让每个对象为null
}
size = 0;
//进行缩容
if (elements !=null && elements.length > DEFAULT_CAPACITY){
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
}
/**
* 缩容
* 当前容量进行缩容 0.5
*/
private void trim(){
int oldCapacity = elements.length;
//缩为原来的一半
int newCapacity = oldCapacity >>1;
//缩到默认容量不再缩小
if (size >=newCapacity || oldCapacity <=DEFAULT_CAPACITY) return;
E[] newElements = (E[]) new Object[oldCapacity];
for (int i=0;i < size; i++){
newElements[i] = elements[i];
}
elements = newElements;
}
......
}
完整代码
/**
* 动态数组
* @param <E> 泛型
*/
public class ArrayList<E> {
/**
* 元素数量
*/
private int size;
/**
* 所有的元素 数组
*/
private E[] elements;
// 设置elements数组默认的初始化空间
private static final int DEFAULT_CAPACITY = 10;
// 表示元素不存在
private static final int ELEMENT_NOT_FOUND = -1;
/**
* 可以自定义数组容量
* @param capacity 容量
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity];
}
/**
* 无参构造默认 10
*/
public ArrayList() {
this(DEFAULT_CAPACITY);
}
/**
* 元素的数量
* @return
*/
public int size(){
return size;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty(){
return size == 0;
}
/**
* 是否包含某个元素
* @param element 元素
* @return
*/
public boolean contains(E element){
return indexOf(element) != ELEMENT_NOT_FOUND;
}
/**
* 添加元素到最后面 --- 优化
* @param element
*/
public void add(E element){
// elements[size++] = element;
add(size,element);
}
/**
* 返回index位置对应的元素 --- 优化
* @param index
* @return
*/
public E get(int index){
rangeCheck(index);
return elements[index];
}
/**
* 设置index位置的元素 --- 优化
* @param index
* @param element
* @return
*/
public E set(int index, E element){
rangeCheck(index);
E old = elements[index];
elements[index] = element;
return old;
}
/**
* 往index位置添加元素 --- 优化
* @param index
* @param element
*/
public void add(int index, E element){
rangeCheckForAdd(index);
//扩容
//当前size=10 如果再添加至少需要11个 (size + 1 是保证至少的容量)
ensureCapacity(size+1);
for (int i = size ; i > index; i--){
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
/**
* 删除index位置对应的元素 --- 优化
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
E old = elements[index];
for (int i = index + 1; i < size; i++) {
elements[i -1] = elements[i];
}
// size -- ;
elements[--size] = null;
//缩容 当前的0.5 但小于等于默认 容量后不再缩
trim();
return old;
}
/**
* 根据元素刪除
* @param element
*/
public void remove(E element){
remove(indexOf(element));
}
/**
* 查看元素的位置 --- 优化
* @param element
* @return
*/
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 (elements[i] == element) return i;
// if (elements[i].equals(element)) return i;
if (element.equals(elements[i])) return i;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 清除所有元素 --- 优化
* 并不是真正的清除 只是将数组元素数量置为0,其实在数组中还是存在原来的数据 ,只不过访问不到而已
* 对对象进行优化
*/
public void clear(){
for (int i=0;i<size;i++){
elements[i] = null;//让每个对象为null
}
size = 0;
//进行缩容
if (elements !=null && elements.length > DEFAULT_CAPACITY){
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
}
/**
* 打印
* @return
*/
@Override
public String toString() {
//Size = ? , [1, 2, 3]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(elements[i]);
// if (i!=size-1){
// string.append(", ");
// }
}
string.append("]");
return string.toString();
}
/**
* 越界异常提示 --- 优化
* @param index
*/
private void outOfBounds(int index){
throw new IndexOutOfBoundsException("Index:"+index+", Size:"+size);
}
/**
* 边界判断 --- 优化
* @param index
*/
private void rangeCheck(int index){
if (index < 0 || index >= size){
this.outOfBounds(index);
}
}
/**
* 添加时边界判断 --- 优化
* @param index
*/
private void rangeCheckForAdd(int index){
if (index < 0 || index > size){
this.outOfBounds(index);
}
}
/**
* 容量扩容自定义的1.5倍: 扩容因子1.5
* 保证要有capacity的容量
* @param capacity
*/
private void ensureCapacity(int capacity){
int oldCapacity = elements.length;
//没有超过默认或者当前数组容量
if (oldCapacity >= capacity) return;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E [] newElements = (E[])new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
System.out.println(oldCapacity+"扩容为"+newCapacity);
}
/**
* 缩容
* 当前容量进行缩容 0.5
*/
private void trim(){
int oldCapacity = elements.length;
//缩为原来的一半
int newCapacity = oldCapacity >> 1;
//缩到默认容量不再缩小
if (size >= newCapacity || oldCapacity <= DEFAULT_CAPACITY) return;
E [] newElements = (E[])new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
System.out.println(oldCapacity+"缩容为"+newCapacity);
}
}
补充:ArrayList能否进一步优化?
- 在ArrayList中,如果要删除索引0位置的元素,则需要将索引0之后的元素全部往前移一位
- 如果要在索引0位置添加元素,也需要将索引0及之后的元素全部往后移一位
- 在ArrayList中增加一个记录首元素位置的属性
- 删除索引0位置的元素,我们只需要将first属性改为1
- 在索引0位置添加元素,则只需要将first属性改为0
- 如果继续往前插入元素,则将插入的元素存放在索引8这个位置,并将first改为8
- 当要获取索引8下一个元素时,对索引取模,则拿到索引0位置的元素
- 如果插入元素,则可选择挪动前半段数据或后半段数据
- 在索引2处插入元素99,可以选择将元素22,33左移,然后插入99即可
- 扩容和缩容同样可以优化