一、數組
關於數組,指的就是一組有限的相關類型的變量集合。在Java語言中,簡單數組並沒有像Collections集合的相關操作接口,本文將對簡單數組封裝相應的操作接口從而形成類ArrayList的集合類爲目標,並進行代碼時間複雜度分析以及代碼優化
在開始之前,我們先對構造的Array類進行成員變量的說明:
- data:核心操作數組成員變量,爲適應多種數據類型數據存儲,將data設置爲泛型數組。
- size:當前數組的元素個數控制指針(手動操作作用於數組上的元素個數,在Java語言中開闢數組會將數組元素進行初始化,這點我們忽略),也就是說size指針會永遠指向當前已存在元素的下一個索引位置。例如:數組容量爲10,當前已存在數組元素爲a[0],a[1],a[2],則size指針便指向 a[3] 元素空間上,並表示當前數組存在元素爲 3 (這裏排除隨機存儲數據元素的情況,以上情況會很大程度上浪費存儲空間,不建議使用)
相應圖解如下:
Part 1 基於泛型的數組類的實現
/**
* @Author: Jiangyanfei
* @Date: 2019/4/26 11:35
* @Version 1.0
*/
public class Array<E> {
/**
* 泛型數組
*/
private E[] data;
/**
* 數組實際元素個數
*/
private int size;
public Array(int capacity) {
data = (E[]) new Object [capacity];
size = 0;
}
public Array() {
this(10);
}
/**
* 獲取數組元素的個數
*/
public int getSize() {
return size;
}
/**
* 獲取數組容量
*/
public int getCapacity() {
return data.length;
}
/**
* 判斷數組是否爲空
*/
public boolean isEmpty() {
return size == 0;
}
}
基礎的屬性方法之後,要開始對常規的操作接口 [CURD] 的設計。先對數組元素的查詢和修改進行方法設計,對於存在數組容量變更的添加和刪除操作在後面會說明。
- 查詢
-
獲取index索引處的元素
/** * 獲取index索引處的元素 */ public E get(int index) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } return data[index]; }
-
查詢數組中是否有元素 e
/** * 查詢數組是否存在元素e */ public boolean contains(E e) { for (E element : data) { if (element.equals(e)) { return true; } } return false; }
-
查詢數組中元素e是否存在,返回索引,不存在返回 -1
/** * 查詢數組中元素e是否存在,返回索引,不存在返回 -1 */ public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; }
-
- 修改
-
改變index索引出的元素值
/** * 改變index索引處的元素值 */ public void set(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } data[index] = e; }
-
Part 2 動態數組實現
在開始正式內容之前,需要先提及一個問題,數組擴容問題?
爲什麼要數組擴容,在數組使用期間,尤其針對添加和刪除操作,定長的數組長度會導致數組空間的浪費,這裏提出一種數組擴容的方法:採用新開數組進行數組元素轉移實現容量擴充或縮減。
採用數組轉移的方式進行擴容方案需要考慮一個問題:擴容的幅度?
假設存在一個容量爲10的數組空間(已滿),這時候新添加數組元素,擴容的幅度爲1?那麼每次元素新添加都要進行擴容操作?反之,刪除元素同理。頻繁的調用擴容方法、大幅度擴大數組容量會造成剩餘空間浪費,我們需要從這兩種極端情況中尋找 折中策略
私有數組擴容方法:resize()
/**
* 數組擴容方法
*/
private void resize(int newCapacity) {
E[] newData = ((E[]) new Object[newCapacity]);
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
resize() 圖示:
數組擴容或縮容的實質就是:數組元素移動到新數組中,並將數組地址指向新數組。
這裏我們默認擴容幅度與縮容幅度倍數爲:2 (僅舉例,可自定義)
結合上述 resize()
方法,繼續完成添加和刪除操作方法的設計:
- 添加
-
指定索引添加元素
/** * 指定索引處添加元素 */ public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } // 數組擴容判斷 if (size == data.length) { resize(data.length * 2); } // 插入元素核心代碼,指定索引後數組元素整體後移 for (int i = size -1; i >= index; i--) { data[i+1] = data[i]; } data[index] = e; size ++; }
-
- 刪除
-
指定索引刪除元素
/** * 指定索引處刪除元素 */ public E remove(int index) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } // 保存待刪除數組元素 E res = data[index]; // 刪除元素核心代碼,指定索引後數組整體前移 for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size --; // 數組縮容判斷 if (size == data.length / 2 && data.length /2 != 0) { resize(data.length / 2); } return res; }
-
Part 3 均攤複雜度淺析以及防止複雜度震動
在提及均攤複雜度之前,首先來分析數組容量變動所引發的時間複雜度變化:
以添加操作爲例:假設數組當前 size 爲 n
- 數組首部插入元素,索引直接定位到 0,索引後數組整體移動範圍爲 n
- 數組尾部插入元素,索引直接定位到 size,元素插入,原數組元素移動範圍 0
- 數組指定索引插入元素,結合上述兩種臨界情況,原數組元素移動範圍 [0, n]
綜合來講,添加操作的時間複雜度爲 O(n),只有在尾部插入元素時,時間複雜度爲 O(1)
那麼我們假設一種情景,數組容量爲 N 的空數組狀態下,依次插入 N+1 個數組元素,並且觸發數組容量擴容操作的流程。
- 首先,在空數組狀態下依次插入 N 個元素,此時操作數:N
- 當數組插入第 N 個元素完成後,準備插入第 N+1 個元素時,觸發
resize()
進行數組容量擴容。完成數組擴容操作後,需要將原數組元素進行轉移,此時操作數變爲了:N + N - 當完成數組轉移之後,插入第 N+1 個元素。此時操作數爲:2N + 1
我們插入了 N+1 個數組元素,總計操作數爲:2N + 1,粗略計算平均每插入一個元素需要耗費的操作數爲:2
那麼這個平均的操作數消耗量我們可以作爲均攤複雜度分析的依據,那麼添加操作的均攤複雜度爲:O(1)
再考慮一種情景,數組容量爲 N 的空數組依次插入 N 個元素,當插入第 N+1 個元素後,便進行刪除第 N+1 個元素的操作,完成之後繼續插入第 N+1 個元素,刪除第 N+1 個元素…如此反覆的進行該流程。
上面說的場景的本質是,擴容和縮容的高頻操作,每次擴容/縮容都需要將數組元素向新數組進行轉移,且轉移的大小與數組長度有關,這一數組轉移操作的時間複雜度穩定在 O(n),那麼反覆的進行該類時間複雜度穩定的操作,會導致複雜度的震動現象。
針對上述複雜度震盪情況,改進下remove()
方法,在數組長度低於 data.length / 2
時觸發resize()
方法。 將觸發的條件 data.length / 2
進行 Lazy
延遲處理 resize()
方法的執行。
if (size == data.length / 4 && data.length /2 != 0) {
resize(data.length / 2);
}
在上述場景的縮容部分中,數組容量爲 2N,resize()
觸發條件爲當數組長度變爲:N/2 時(容量四分點)觸發。
注:data.length /2 != 0
是確保不構造容量爲 0 的新數組