靜態陣列
Java中最基本的陣列大家肯定不會陌生:
int[] array = new int[6];
for (int i = 0; i < array.length; i++){
array[i] = 2 * i + 1;
}
通過迴圈把元素放入指定的位置中,類似於這樣:
這是一個靜態陣列,因為我們在第一步初始化的時候就已經固定了它的長度,後面再也無法改變。所以,由於有這個限制,靜態陣列不適用於那些不確定儲存多少資料的場景。
但是如果陣列滿了,能否再新建一個更長一些的陣列,把原陣列這些元素再轉移到新陣列中呢?這樣一來,陣列就可以繼續使用了。按照這個思路,我們就可以建立基於靜態陣列的動態陣列。
動態陣列的實現原理
“動態”主要體現在以下幾方面:
1.新增元素
不侷限於只在陣列末尾新增,而是能夠隨意選擇索引位置(只要不超過陣列長度)。例如在索引為1處新增元素4:
從圖中可以看出,需要將index處及右側的元素依次向右移動一個單位(從末位元素開始),最後用新增元素覆蓋index處元素。
2.刪除元素
同新增元素,也可根據索引進行選擇。例如刪除索引為0處的元素3:
刪除元素移動元素的方向與新增元素正好相反,從index處開始,直接使用後一位元素覆蓋前一位元素,最後將末位元素置為null。
3.陣列擴容
陣列一旦裝滿元素,可觸發陣列擴容,即新建一個更長的陣列,將原陣列元素轉移到新陣列中,並將引用指向新陣列,完成陣列的變更;
4.陣列縮減
如果陣列元素相對總容量來說過少(例如陣列元素個數小於陣列容量的1/4),便可觸發陣列縮減,即新建一個更短的陣列,並轉移元素至新陣列。
程式碼實現
以下通過新建一個 Array 類,依次實現這幾個重要功能:
public class Array<E> {
private E[] data; // 使用靜態陣列存放陣列元素
private int size; // 記錄陣列元素數量
public Array(int capacity) {
this.data = (E[]) new Object[capacity];
this.size = 0;
}
public Array() {
this(10); // 預設capacity為10
}
// 陣列擴容/縮減
public void resize(int newCapacity) {
// 新陣列長度必須大於0
if (newCapacity < 0) throw new IllegalArgumentException("capacity must > 0!");
// 建立新陣列
E[] newData = (E[]) new Object[newCapacity];
// 將原陣列元素放入新陣列中
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
// 將引用指向新陣列
data = newData;
}
/**
* 在指定位置新增元素
* 指定位置處的元素需要向右側移動一個單位
* @param index 索引
* @param element 要新增的元素
*/
public void add(int index, E element) {
if (index < 0 || index > size) throw new IllegalArgumentException("Illegal index, index must > 0 and <= size!");
// 陣列滿員觸發擴容
if (size == data.length) {
resize(2 * data.length); // 擴容為原陣列的2倍
}
// 從尾部開始,向右移動元素,直到index
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
// 新增元素
data[index] = element;
size++;
}
// 陣列頭部新增元素
public void addFirst(E element) {
add(0, element);
}
// 陣列尾部新增元素
public void addLast(E element) {
add(size, element);
}
/**
* 刪除指定位置元素
* 通過向左移動一位,覆蓋指定位置處的元素,實現刪除元素(data[size - 1] = null)
* @param index 索引
*/
public E remove(int index) {
if (index < 0 || index > size) throw new IllegalArgumentException("Illegal index, index must > 0 and < size!");
// 陣列長度為0時丟擲異常
if (size == 0) throw new IllegalArgumentException("Empty array!");
E removedElement = data[index];
// 向左移動元素
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
// 將尾部空閒出的位置置為空,釋放資源
data[size - 1] = null;
size--;
// size過小觸發陣列縮減
if (size == data.length / 4 && data.length / 2 != 0) resize(data.length / 2);
return removedElement;
}
// 刪除頭部元素
public E removeFirst() {
return remove(0);
}
// 刪除尾部元素
public E removeLast() {
return remove(size - 1);
}
// 重寫Override方法,自定義陣列顯示格式
@Override
public String toString() {
StringBuilder str = new StringBuilder();
// 顯示陣列的整體情況(長度、總容量)
str.append(String.format("Array: size = %d, capacity = %d\n[", size, data.length));
// 迴圈新增陣列元素至str
for (int i = 0; i < size; i++) {
str.append(data[i]);
if (i < size - 1) str.append(", ");
}
str.append("]");
return str.toString();
}
}
接下來我們測試一下這個陣列的使用情況:
public static void main(String[] args) {
// 新增10個元素
Array<Integer> arr = new Array<>();
for (int i = 0; i < 10; i++)
arr.add(i, i);
// 檢視陣列當前狀態
System.out.println(arr);
// 繼續新增元素,觀察是否擴容
arr.add(arr.size, 7);
System.out.println(arr);
// 再刪除6個元素,觀察是否縮減
for (int i = 0; i < 6; i++) {
System.out.println("元素" + arr.removeFirst() + "已被刪除!");
}
System.out.println(arr);
}
/*
輸出結果:
Array: size = 10, capacity = 10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Array: size = 11, capacity = 20
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 7]
元素0已被刪除!
元素1已被刪除!
元素2已被刪除!
元素3已被刪除!
元素4已被刪除!
元素5已被刪除!
Array: size = 5, capacity = 10
[6, 7, 8, 9, 7]
*/
可以看到,當陣列滿員後,繼續新增元素可以成功觸發陣列擴容;而當陣列元素過少時,也會觸發縮減。
再實現幾個常用方法來完善我們的動態陣列類:
// 獲取陣列長度
public int getSize() {
return size;
}
// 獲取陣列總容量
public int getCapacity() {
return data.length;
}
// 判斷陣列是否為空
public boolean isEmpty() {
return getSize() == 0;
}
// 查詢指定元素在陣列中的位置
public int search(E element) {
for (int i = 0; i < getSize(); i++) {
if (data[i].equals(element)) {
return i;
}
}
// -1表示未找到
return -1;
}
// 判斷指定元素是否在陣列中
public boolean contains(E element) {
return search(element) != -1;
}
// 按照索引查詢元素值
public E get(int index) {
if (index < 0 || index > size) throw new IllegalArgumentException("Illegal index, index must > 0 and < size!");
return data[index];
}
// 查詢頭部元素
public E getFirst() {
return get(0);
}
// 查詢尾部元素
public E getLast() {
return get(getSize() - 1);
}
// 設定指定位置的元素值
public void set(int index, E element) {
if (index < 0 || index > size) throw new IllegalArgumentException("Illegal index, index must > 0 and < size!");
data[index] = element;
}
/**
* 按照元素值刪除
* 只刪除陣列中第一個元素值與指定值相等的元素
* @param element 指定元素值
*/
public boolean removeElement(E element) {
int index = search(element);
if (index != -1) {
remove(index);
return true;
}
return false;
}
/**
* 按照元素值刪除
* 刪除陣列中所有值與指定值相等的元素
*
* @param element 指定元素值
*/
public boolean removeElementAll(E element) {
boolean isRemoved = false;
int i = getSize() - 1;
while (i >= 0) {
if (data[i].equals(element)) {
remove(i);
isRemoved = true;
}
i--;
}
return isRemoved;
}
從外部呼叫者的角度,無法覺察到其中的陣列變更操作,感覺就是一個動態陣列,但是由於擴容和縮減操作均需要新建陣列,並且遍歷原陣列,會導致過多的開銷,所以從效能上來說,並不是好的解決方案。後面我們將學習更加高效的資料結構。