目录
数组是用于储存多个相同类型数据的集合,可以通过索引来访问数组中具体的元素。但是因为数组在一创建时就需要明确数组的大小(开辟一块连续的空间),所以数组在程序中使用起来就具有局限性。
数组就是把数据码成一排进行存放。
数组最大的优势就是可以快速查询,在数组中,每个数据存放位置的索引是没有语义的(仅表示位置,没有业务逻辑语义,比如不适合表示学号,身份证号等),因此,往往在实际开发中,不直接使用数组,而是使用基于数组结构封装的列表容器,比如 Java 中的 ArrayList 。
1、基于数组列表的元素添加
Java 中 ArrayList 类的添加方法实现
在 Java 中,如果向下标大于列表中实际数据量的位置添加数据,会抛出 IndexOutOfBoundsException 异常
(1)向数组的末尾添加元素,只需要维护数组实际容量指针就可以了,不需要移动数据,效率很高
![](https://i-blog.csdnimg.cn/blog_migrate/faa8a274e990374059385ae47f08cef5.png)
(2)向数组的指定位置添加元素,假如向队首添加元素的话,将需要移动整个数组的数据
![](https://i-blog.csdnimg.cn/blog_migrate/2e6efaba0b8c27db4a17b24241074b9c.png)
2、基于数组列表的元素查找
Java 中 ArrayList 类的查找方法实现,直接使用索引下标,在Java中,如果列表索引位置不存在数据,会抛出 IndexOutOfBoundsException 异常
使用下标的数据访问,查找效率很快
3、基于数组列表的元素修改
Java 中 ArrayList 类的修改方法实现,也是通过下标,一步到位
通过下标修改数据,直接定位到下标所在位置,进行元素替换,如 arr.set(4,68),在Java中,如果列表索引位置不存在数据,会抛出 IndexOutOfBoundsException 异常
4、基于数组列表的元素包含判断
Java 中 ArrayList 类的包含方法实现,需要遍历整个列表
例如,查找列表中是否包含 68 的元素,会遍历整个列表,查找是否有等于 68 的元素
5、基于数组列表的元素删除
Java 中 ArrayList 类的删除方法实现,可以根据下标删除,也可以根据对象删除,根据对象删除需要遍历整个列表,如果不是删除队尾数据,还需要移动列表中其他元素
例如,删除下标为 2 的元素,下标2后边的元素都会向前移动,保持数组的连续性,删除的数据下标越小,移动的数据越多,性能越差
删除操作,是增加的反向操作,把需要删除元素后边的元素向前移动,同时维护 size-- ;移动后,最后一个元素它还存在(置灰的 17),但是没有关系,因为维护了 size ,此时 size 指示位置的元素永远也访问不到。
6、基于数组列表的动态扩容
静态数组转变为可动态扩容的动态数组,主要是为了解决使用静态数组容量固定不灵活问题。
实际业务场景中,往往无法精确预估需要存储的数据量,如果数组容量开的太大,会浪费存储空间,如果容量开得太小,又会导致数组的容量不够用。因此,使用数组封装的数据结构,都需要考虑合适的动态扩容问题。
Java 中 ArrayList 类的容量扩容
ArrayList 类每次进行 add() 操作时,都会对数组容量进行一次判断,如果元素个数+1 <= Capacity,那就可以放心添加元素
当元素个数+1 > Capacity(总容量) ,那 ArrayList 就发生扩容,容量 Capacity 变为 1.5 倍Capacity。核心代码:newCapacity = oldCapacity + (oldCapacity >> 1);对 oldCapacity 进行位运算,左移一位,所以 oldCapacity >>1 其实就是 oldCapacity / 2 ,所以 oldCapacity + (oldCapacity >> 1) 就等于1.5 oldCapacity);
Java 中 ArrayList 类的容量缩容
在 Java 中并没有提供自动缩容方式,可以手动调用 trimToSize() 方法,这个方法会将 ArrayList 内置数组缩容到当前的 size 大小
列表的动态扩容机制如下图,扩容需要复制之前数组中的所有元素,尽量避免频繁扩容
缩容和扩容带来的时间复杂度震荡
问题出现,假如,当数组需要扩容时,扩充新数组为原来容量的2倍,另外,又指定存储的数组元素为现有数组容量的1/2时,触发缩容,数组容量缩减为原来容量的1/2。// 缩放比一样
这种情况下,如果程序恰好在数组扩容的这个位置上进行元素的增删操作,就意味着每次操作都会伴随着数组的扩容和缩容,此时方法的时间复杂就会提升为O(n^2)。这种现象称为时间复杂度的震荡。
解决方案:采用不相同的缩放比率,如果当数组的容量为1/4时,才将数组的容量缩减为原来的一半,正因为如此,即便是缩容后再添加元素也不需要立即扩充数组的容量。
避免缩容过于着急,把缩容的条件由1/2变为1/4,采用更加懒惰的方案。
附:自定义封装数组的列表实现
public class Array<E> { // 使用泛型
// 基于java的数组进行二次封装,private类型不允许外部修改
private E[] data;
// 维护一个size,指定了数组中存放了多少元素
private int size;
// 构造函数,传入数组的容量capacity构造Array
public Array(int capacity) {
data = (E[]) new Object[capacity];
size = 0;
}
// 无参数的构造函数,默认数组的容量capacity=10
public Array() {
this(10);
}
// 获取数组的容量
public int getCapacity() {
return data.length;
}
// 获取数组中的元素个数
public int getSize() {
return size;
}
// 返回数组是否为空
public boolean isEmpty() {
return size == 0;
}
// 在index索引的位置插入一个新元素e
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
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++;
}
// 向所有元素后添加一个新元素
public void addLast(E e) {
add(size, e);
}
// 在所有元素前添加一个新元素
public void addFirst(E e) {
add(0, e);
}
// 获取index索引位置的元素
public E get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get failed. Index is illegal.");
return data[index];
}
// 修改index索引位置的元素为e
public void set(int index, E e) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Set failed. Index is illegal.");
data[index] = e;
}
// 查找数组中是否有元素e
public boolean contains(E e) {
for (int i = 0; i < size; i++) {
if (data[i].equals(e))
return true;
}
return false;
}
// 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
public int find(E e) {
for (int i = 0; i < size; i++) {
if (data[i].equals(e))
return i;
}
return -1;
}
// 从数组中删除index位置的元素, 返回删除的元素
public E remove(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal.");
E ret = data[index];
for (int i = index + 1; i < size; i++)
data[i - 1] = data[i];
size--;
data[size] = null; // loitering objects != memory leak
// 当实际存储元素只有数组容量的1/4时,触发缩容,且缩容的数量量不能为 0
if (size == data.length / 4 && data.length / 2 != 0)
resize(data.length / 2); // 缩容
return ret;
}
// 从数组中删除第一个元素, 返回删除的元素
public E removeFirst() {
return remove(0);
}
// 从数组中删除最后一个元素, 返回删除的元素
public E removeLast() {
return remove(size - 1);
}
// 从数组中删除元素e
public void removeElement(E e) {
int index = find(e);
if (index != -1)
remove(index);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
res.append('[');
for (int i = 0; i < size; i++) {
res.append(data[i]);
if (i != size - 1)
res.append(", ");
}
res.append(']');
return res.toString();
}
// 将数组空间的容量变成 newCapacity 大小
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++)
newData[i] = data[i];
data = newData;
}
}
测试自定义封装数组的列表
public class Main {
public static void main(String[] args) {
Array<Integer> arr = new Array<>();
for (int i = 0; i < 10; i++)
arr.addLast(i); // 向列表添加元素
System.out.println("初始数组:" + arr);
arr.add(1, 100); // 再次向列表添加元素,触发扩容
System.out.println("触发扩容:" + arr);
arr.addFirst(-1); // 向队首添加元素
System.out.println(arr);
arr.remove(2); // 移除指定位置元素
System.out.println(arr);
arr.removeElement(4); // 移除元素
System.out.println(arr);
arr.removeFirst(); // 移除队首元素
System.out.println(arr);
for (int i = 0; i < 4; i++) {
arr.removeFirst(); // 4次移除元素,达到缩容条件
System.out.println(arr);
}
}
}
至此,数据结构之数组和列表介绍完毕。