算法01:动态数组

97 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

学习恋上数据结构与算法笔记

1.什么是数据结构?

数据结构是计算机存储、组织数据的方式

image-20220308002540373

在实际应用中,根据使用场景来选择最合适的数据结构

2.线性表

线性表是具有nn个相同类型元素的有限序列(n0n \geq 0)

image-20220308002635126

a1a_1 是首节点(首元素), ana_n 是尾结点(尾元素) a1a_1a2a_2 的前驱, a2a_2a1a_1 的后继

常见的线性表有:

  • 数组
  • 链表
  • 队列
  • 哈希表(散列表)

3.数组(Array)

数组是一种顺序存储的线性表,所有元素的内存地址是连续

int[] array = new int[]{11, 22, 33};

image-20220308002910022

在很多编程语言中,数组都有个致命的缺点:无法动态修改容量

实际开发中,我们更希望数组的容量是可以动态改变

4.动态数组(Dynamic Array)接口设计

image-20220308005738990

函数说明
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();清除所有元素

编程思想

尽量避免魔数,静态常量单独拿出来声明,字母大写,中间下划线连接。

private static final int DEFAULT_CAPACITY = 10;   // 默认容量
private static final int ELEMENT_NOT_FOUND = -1;  // 没有找到

构造器之间使用this来复用代码,为了增强鲁棒性,给capacity初始值如果小于10就默认设置为10

public ArrayList(int capacity) {
    // 如果初始容量小于10则默认认为初始容量为10
    capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
    elements = (E[]) new Object[capacity];
}
 
public ArrayList() {
    // elements = new int[DEFAULT_CAPACITY];
    this(DEFAULT_CAPACITY);
}

如果使用get取元素的时候下标越界,直接抛出异常即可。方便定位错误位置。

public E get(int index) {
    // 下标越界
    if(index < 0 || index >= size) {
        throw new IndexOutOfBoundsException("Index: " + index + ", Size:" + size);
    }
    return elements[index];
}

size = 0可以表示清空,当然也可以置为null和重新new但是那样效率太低了,总而言之,实现的方式很多,需要具体情况具体分析。这就相当于是设计一个框架,内部的实现对外部是不可见的,我们是内部的实现。

public void clear() {
    // 即使后面有数据也访问不到
    size = 0;
}

5. 动态数组的设计

Java中,成员变量会自动初始化,比如int类型自动初始化为 0,对象类型自动初始化为 null

5.1 添加元素 - add(E element)

image-20220308122517079

后续可以优化为直接调用后面已经写好的方法add(index, ele)

5.2 打印数组

重写toString方法

toString方法中将元素拼接成字符串

字符串拼接建议使用 StringBuilder

@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]);
        // if(i != size - 1) {
        //     string.append(", ");
        // }
    }
    string.append("]");
    return string.toString();
}

推荐使用if(i != 0)的写法,原因是效率较高,相比于下面的写法不用多做一次减法运算size - 1

5.3 删除元素 - remove(int index)

image-20220308124018228

思考:最后一个元素如何处理? 不需要处理 比如这里剩下的 77,因为size--了,无法访问到最后一个元素了

数组的内存分配是连续的,只能是数组里面的元素往前挪动。

5.4 添加元素 - add(int index, E element)

注意挪动的顺序,与添加元素是相反的。

image-20220308131236416

public void add(int index, int 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++;
}

注意这里的判断条件是index > size表示可以在最后一个位置上插入元素list.add(list.size(), 100);

由于代码中有很多对于size的判断,属于重复代码,将这一部分进行封装:

/**
* 封装同样的异常
*
* @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) {
        outOfBounds(index);
    }
}
 
/**
* 添加范围检查
*
* @param index
*/
private void rangeCheckForAdd(int index) {
    if(index < 0 || index > size) {
        outOfBounds(index);
    }
}

接口测试:

public class Asserts {
    public static void test(boolean value) {
        try {
            if (!value) throw new Exception("测试未通过");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

调用Asserts.test(list.get(3) == 80);进行测试

5.5 如何扩容

重新申请一块更大的内存空间。

image-20220308131510810

注意这里不能用sizecapacity进行比较,因为size存储的是具体元素有多少个,而应该用此时数组能够存储的容量elements.lengthcapacity进行比较

5.6 泛型

如何创建泛型数组?

elements = new E[capacity];  错误
elements = (E[]) new Object[capacity];

所有的类,最终都继承java.lang.Object

5.7 对象数组

改为泛型之后多了一个对象的内存管理的问题。如果是new int[10]很明显知道需要分配4 * 10共40个字节的空间,但是如果是对象数组的话,new Object[10]不知道分配的空间?new Object[7]里面存放的是对象的地址值。

每个对象的内存地址大小是一样的,比如说都占8个字节。

image-20220308231926991

5.8 内存管理细节

如何销毁这个对象,把这个线断掉。将对应位置上的值赋为null

如果栈空间中的objects数组到堆空间中的指向断掉了,那么数组首先会被回收,然后数组中引用的对象被回收。

此时当clear的时候需要回收内存。之前由于数组中的每个元素都是int,是基本类型的数据,无需额外处理,直接size = 0就好,但是如果数组中存储的是对象地址时,这些对象会一直在内存中。

public void clear() {
    for (int i = 0; i < size; i++) {
        elements[i] = null;
    }
    size = 0;
}

clear细节:

/**
* 对象死之前要做什么事情
*
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
    super.finalize();
 
    System.out.println("Person - finalize");
}
// 提醒JVM进行垃圾回收
// 检查哪些对象没有被指向,没有被指向的通通被回收内存
System.gc();

删除remove的细节:

将最后位置上的元素赋值为null,否则该元素仍然被引用,无法被垃圾回收

元素比较是否相等:

重写equals方法

@Override
public boolean equals(Object obj) {
    if (obj == null) return false;
    if (obj instanceof Person) {
        Person person = (Person) obj;
        return this.age == person.age;
    }
    return false;
}

修改方法:

/**
* 查看元素的索引
*/
public int indexOf(E element) {
    for (int i = 0; i < elements.length; i++) {
        if (elements[i].equals(element)) return i;
    }
    return ELEMENT_NOT_FOUND;
}

5.9 null 的处理

数组中能够存放空元素?这是一个内部设计的问题。

如果数组中运行添加null元素,如果数组中存在多个null元素,那么indexOf方法中存在问题:

/**
* 查看元素的索引
*/
public int indexOf(E element) {
    for (int i = 0; i < elements.length; i++) {
        // null无法调用equals方法,会报空指针异常
        if (elements[i].equals(element)) return i;
    }
    return ELEMENT_NOT_FOUND;
}

6. java.util.ArrayList

modCount只有在迭代器里面才有用

移除某个元素

public void remove(E element) {
    remove(indexOf(element));
}