一篇文章讓你瞭解動態陣列的資料結構的實現過程(Java 實現)
阿新 • • 發佈:2020-04-02
[TOC]
## 陣列基礎簡單回顧
1. 陣列是一種資料結構,用來儲存**同一型別值**的集合。
2. 陣列就是**儲存資料長度固定的容器**,保證**多個數據的資料型別要一致**。
3. 陣列是一種**引用資料型別**。
4. 簡單來說,陣列就是把需要儲存的資料排成一排進行存放。
5. 陣列的索引從 **0** 開始計數,最後一個位置的索引是**陣列的長度 - 1(n - 1)**。
6. 可以使用陣列的**索引**來存取資料。
7. 陣列的索引**可以有語意,也可以沒有語意**。
- 例如,一個儲存學生成績的陣列如果索引有語意,那麼索引可以看成學生的學號,此時對於使用索引運算元組就可以看成對學號是 xxx 的學生進行存取成績的操作。那麼如果沒有語意,就是隨意存取學生的成績到該陣列中。
8. 陣列最大的優點:**快速查詢**。例如:arr[index]。
- 根據此優點,可以知道陣列最好應用於 **“索引有語意”** 的情況。因為索引有了語意,那麼我們就可以知道要取的資料是什麼、是在哪個位置,可以很方便地查詢到資料。
- **但也並非是所有有語意的索引都適用於陣列**,例如身份證號。我們知道,一個身份證號的號碼有 18 位的長度,如果索引為一個身份證號,其對應著一個人,那麼陣列就要開啟很大的空間,要開啟空間到一個索引能有 18 位長度的數字這麼大。那麼此時如果只存取幾個人,空間就會很浪費,而且這麼多空間裡並不是每一個索引都能是一個身份證號,有些是和身份證號的格式對應不上的,這些空間就會被浪費,所以並非是所有有語意的索引都適用於陣列,要根據情況來決定使用。
9. **陣列也可以處理“索引沒有語意”的情況**。比如一個數組有 10 個空間,其中前 4 個空間有資料,此時索引沒有語意,對於使用者而言,後面的空間是沒有元素的,那麼此時如何處理我們需要進行考慮。所以我們可以根據 Java 的陣列來二次封裝一個數組類來進行處理“索引沒有語意”的情況,以此掌握陣列這個資料結構。
---
## 二次封裝陣列類設計
### 基本設計
- 這裡我將這個封裝的陣列類取名為 Array,其中封裝了一個 Java 的**靜態陣列 data[] 變數**,然後基於這個 data[] 進行二次封裝實現增、刪、改、查的操作。接下來將一一實現。
- **成員變數設計**
- 由於陣列本身是靜態的,在建立的時候需指定大小,此時我將這個大小用變數 **capacity** 表示,即容量,表示陣列空間最多裝幾個元素。但並不需要在類中宣告,只需在建構函式的引數列表中宣告即可,因為**陣列的容量也就是 data[] 的長度**,不需要再宣告一個變數來進行維護。
- 對於陣列中實際擁有的元素個數,這裡我用變數 **size** 來表示。初始時其值為 **0**。
- 這個 **size** 也能表示為**陣列中第一個沒有存放元素的位置**。
- 例如陣列為空時,size 為 0,此時索引 0 處為陣列中第一個沒有存放元素的位置;再如陣列中有兩個元素時,size 為 2,此時索引 0 和 1 處都有元素,索引 2 處沒有,也就是陣列中第一個沒有存放元素的位置。
- 所以可先建立 Array 類如下所示:
```java
/**
* 基於靜態陣列封裝的陣列類
*
* @author 踏雪彡尋梅
* @date 2019-12-17 - 22:26
*/
public class Array {
/**
* 靜態陣列 data,基於該陣列進行封裝該陣列類
* data 的長度對應其容量
*/
private int[] data;
/**
* 陣列當前擁有的元素個數
*/
private int size;
/**
* 預設建構函式,使用者不知道要建立多少容量的陣列時使用
* 預設建立容量為 10 的陣列
*/
public Array() {
// 預設建立容量為 10 的陣列
this(10);
}
/**
* 建構函式,傳入陣列的容量 capacity 構造 Array
* @param capacity 需要開闢的陣列容量,由使用者指定
*/
public Array(int capacity) {
// 初始化 data[] 和 size
data = new int[capacity];
size = 0;
}
/**
* 獲得陣列當前的元素個數
* @return 返回陣列當前的元素個數
*/
public int getSize() {
return size;
}
/**
* 獲得陣列的容量
* @return 返回陣列的容量
*/
public int getCapacity() {
// data[] 的長度對於其容量
return data.length;
}
/**
* 判斷陣列是否為空
* @return 陣列為空返回 true;否則返回 false
*/
public boolean isEmpty() {
// 當前 data[] 的元素個數為 0 代表陣列為空,否則非空
return size == 0;
}
}
```
---
### 向陣列中新增元素
- 對於向陣列中新增元素,**向陣列末尾新增元素**是最簡單的,原理如下:
- 顯而易見,往陣列末尾新增元素是新增操作中最簡單的操作,因為我們已經知道 **size 這個變數指向的是陣列第一個沒有元素的地方**,很容易理解,**size 這個位置就是陣列末尾的位置**,所以往這個位置新增元素時也就是往陣列末尾新增元素了,新增後維護 size 的值將其加一即可。**當前新增時也需要注意陣列空間是否已經滿了。**
- 新增過程如下圖所示:
![陣列末尾插入元素演示](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9teWJsb2dwaWN0dXJlLm9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20vJUU2JTk1JUIwJUU2JThEJUFFJUU3JUJCJTkzJUU2JTlFJTg0X0phdmFfJUU2JTk1JUIwJUU3JUJCJTg0LyVFNSVCRSU4MCVFNiU5NSVCMCVFNyVCQiU4NCVFNiU5QyVBQiVFNSVCMCVCRSVFNiU4RiU5MiVFNSU4NSVBNSVFNSU4NSU4MyVFNyVCNCVBMCVFNiVCQyU5NCVFNyVBNCVCQSVFNSU5QiVCRS5naWY)
- 用程式碼來表示就如下所示:
```java
/**
* 向陣列末尾新增一個新元素
* @param element 新增的新元素
*/
public void addLast(int element) {
// 檢查陣列空間是否已滿
if (size == data.length) {
// 丟擲一個非法引數異常表示向陣列末尾新增元素失敗,因為陣列已滿
throw new IllegalArgumentException("AddLast failed. Array is full.");
}
// 在陣列末尾新增新元素
data[size] = element;
// 新增後維護 size 變數
size++;
}
```
- 當然,也不能總是往陣列末尾新增元素,當**使用者有往指定索引位置新增元素的需求**時,也要將其實現:
- 對於往指定索引位置新增元素:首先需要做的便是**將該索引位置及其後面所有的元素都往後面移一個位置**,將這個索引位置空出來。
- **需要注意:並不是真的空出來,這個位置如果之前有元素的話還是存在原來的元素,只不過已經為原來元素製作了一個副本並將其往後移動了一個位置。**
- 其次再將元素新增到該索引位置。
- **如果這個位置之前有元素的話實質上就是將新元素覆蓋到原來的元素上。**
- 最後再維護儲存陣列當前元素個數的變數 size 將其值加一。
- 當然在**插入的時候也要確認陣列是否有足夠的空間以及確認插入的索引位置是否合法(該位置的合法值應該為 0 到 size 這個範圍)。**
- 具體過程如下圖所示:
![往陣列指定位置新增元素演示](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9teWJsb2dwaWN0dXJlLm9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20vJUU2JTk1JUIwJUU2JThEJUFFJUU3JUJCJTkzJUU2JTlFJTg0X0phdmFfJUU2JTk1JUIwJUU3JUJCJTg0LyVFNSVCRSU4MCVFNiU5NSVCMCVFNyVCQiU4NCVFNiU4QyU4NyVFNSVBRSU5QSVFNCVCRCU4RCVFNyVCRCVBRSVFNiVCNyVCQiVFNSU4QSVBMCVFNSU4NSU4MyVFNyVCNCVBMCVFNiVCQyU5NCVFNyVBNCVCQSVFNSU5QiVCRS5naWY)
- 用程式碼來表示該過程就如下所示:
```java
/**
* 在陣列的 index 索引處插入一個新元素 element
* @param index 要插入元素的索引
* @param element 要插入的新元素
*/
public void add(int index, int element) {
// 檢查陣列空間是否已滿
if (size == data.length) {
// 丟擲一個非法引數異常表示向陣列指定索引位置新增元素失敗,因為陣列已滿
throw new IllegalArgumentException("Add failed. Array is full.");
}
// 檢查 index 是否合法
if (index < 0 || index > size) {
// 丟擲一個非法引數異常表示向陣列指定索引位置新增元素失敗,應該讓 index 在 0 到 size 這個範圍才行
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
}
// 將 index 及其後面所有的元素都往後面移一個位置
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
// 將新元素 element 新增到 index 位置
data[index] = element;
// 維護 size 變數
size++;
}
```
- 在實現這個方法之後,**對於之前實現的 addLast 方法又可以進行簡化了**,只需在其中**複用 add 方法**,將 size 變數和要新增的元素變數 element 傳進去即可。如下所示:
```java
/**
* 向陣列末尾新增一個新元素
* @param element 新增的新元素
*/
public void addLast(int element) {
// 複用 add 方法實現該方法
add(size, element);
}
```
- 同理,也可再依此實現一個方法實現**往陣列首部新增一個新元素**,如下所示:
```java
/**
* 在陣列首部新增一個新元素
* @param element 新增的新元素
*/
public void addFirst(int element) {
// 複用 add 方法實現該方法
add(0, element);
}
```
- 對於新增操作的基本實現,已經編寫完成,接下來就繼續實現在陣列中查詢元素和修改元素這兩個操作。
---
### 在陣列中查詢元素和修改元素
- 查詢元素時我們**需要直觀地知道陣列中的資訊**,所以在查詢元素和修改元素之前需要先重寫 toString 方法,以讓後面我們可以直觀地看到陣列中的資訊,實現如下:
```java
/**
* 重寫 toString 方法,顯示陣列資訊
* @return 返回陣列中的資訊
*/
@Override
public String toString() {
StringBuilder arrayInfo = new StringBuilder();
arrayInfo.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
arrayInfo.append("[");
for (int i = 0; i < size; i++) {
arrayInfo.append(data[i]);
// 判斷是否為最後一個元素
if (i != size - 1) {
arrayInfo.append(", ");
}
}
arrayInfo.append("]");
return arrayInfo.toString();
}
```
- 那麼接下來就可以實現這些操作了,首先先實現**查詢的方法**:
- 這裡實現一個**獲取指定索引位置的元素的方法**提供給使用者用於查詢指定位置的元素:
- 對於這個方法,我們知道這個類是基於一個靜態陣列 data[] 進行封裝的,那麼對於獲取指定索引位置的元素,我們只需**使用 data[index] 就可獲取到相應的元素,並且對使用者指定的索引位置 index 進行合法性檢測即可。**
- 同時,對於 data 我們之前已經做了 private 處理,那麼使用該方法來封裝獲取元素的操作也可以**避免使用者直接對 data 進行操作**,並且在此方法中進行了 idnex 的合法性檢測。那麼**對於使用者而言,對於陣列中未使用的空間,他們是永遠訪問不到的,這保證了資料的安全,他們只需知道陣列中已使用的空間中的元素能夠進行訪問即可。**
- 具體程式碼實現如下:
```java
/**
* 獲取 index 索引位置的元素
* @param index 要獲取元素的索引位置
* @return 返回使用者指定的索引位置處的元素
*/
public int get(int index) {
// 檢查 index 是否合法
if (index < 0 || index >= size) {
// 丟擲一個非法引數異常表示獲取 index 索引位置的元素失敗,因為 index 是非法值
throw new IllegalArgumentException("Get failed. Index is illegal.");
}
// 返回使用者指定的索引位置處的元素
return data[index];
}
```
- 同理,可以實現**修改元素的方法**如下:
```java
/**
* 修改 index 索引位置的元素為 element
* @param index 使用者指定的索引位置
* @param element 要放到 index 處的元素
*/
public void set(int index, int element) {
// 檢查 index 是否合法
if (index < 0 || index >= size) {
// 丟擲一個非法引數異常表示修改 index 索引位置的元素為 element 失敗,因為 index 是非法值
throw new IllegalArgumentException("Set failed. Index is illegal.");
}
// 修改 index 索引位置的元素為 element
data[index] = element;
}
```
- 該方法實現的內容則是**修改指定位置的老元素為新元素,同樣也進行了 index 的合法性檢測,對於使用者而言是修改不了陣列的那些未使用的空間的。**
- 實現了以上方法,就可以接著實現陣列中的包含、搜尋和刪除這些方法了。
---
### 陣列中的包含、搜尋和刪除元素
- 在很多時候,我們在陣列中儲存了許多元素,**有時需要知道這些元素中是否包含了某個元素**,這時候就要**實現一個方法來判斷陣列中是否包含我們需要的元素了**:
- 對於該方法,實現起來也很容易,只需**遍歷整個陣列,逐一判斷是否包含有需要的元素**即可,實現如下:
```java
/**
* 查詢陣列中是否有元素 element
* @param element 使用者需要知道是否存在於陣列中的元素
* @return 如果陣列中包含有 element 則返回 true;否則返回 false
*/
public boolean contains(int element) {
// 遍歷陣列,逐一判斷
for (int i = 0; i < size; i++) {
if (data[i] == element) {
return true;
}
}
return false;
}
```
- 不過有些時候使用者**不僅需要知道陣列中是否包含需要的元素,還需要知道其所在的索引位置處**,這時候就要實現一個方法來**搜尋使用者想要知道的元素在陣列中的位置**了:
- 對於這個方法,具體實現和上面的包含方法差不多,也是**遍歷整個陣列然後逐一判斷,不同的是如果存在需要的元素則是返回該元素的索引,如果不存在則返回 -1 表示沒有找到**,實現如下:
```java
/**
* 查詢陣列中元素 element 所在的索引
* @param element 進行搜尋的元素
* @return 如果元素 element 存在則返回其索引;不存在則返回 -1
*/
public int find(int element) {
// 遍歷陣列,逐一判斷
for (int i = 0; i < size; i++) {
if (data[i] == element) {
return i;
}
}
return -1;
}
```
- 最後,則實現**在陣列中刪除元素的方法**,先實現**刪除指定位置元素的方法**:
- 對於刪除指定位置元素這個方法,其實和之前實現的在指定位置新增元素的方法的思路差不多,只不過反轉了過來。
- 對於刪除來說,只需**從指定位置後一個位置開始,把指定位置後面的所有元素一一往前移動一個位置覆蓋前面的元素**,最後再維護 size 將其值減一併且**返回刪除的元素**,就完成了刪除指定位置的元素這個操作了,當然**也需要進行指定位置的合法性判斷**。
- 此時完成了刪除之後,**雖然 size 處還可能含有刪除之前的陣列的最後一個元素或者含有陣列的預設值。(建立陣列時,每個位置都有一個預設值 0)**。但對使用者而言,這個資料他們是拿不到的。因為**對於獲取元素的方法,已經設定了 index 的合法性檢測,其中限制了 index 的範圍為大於等於 0 且小於 size,所以 size 這個位置的元素使用者是取不到的**。綜上該位置如含有之前的元素是不影響接下來的操作的。
- 具體過程圖示如下:
![刪除陣列指定位置元素演示](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9teWJsb2dwaWN0dXJlLm9zcy1jbi1zaGVuemhlbi5hbGl5dW5jcy5jb20vJUU2JTk1JUIwJUU2JThEJUFFJUU3JUJCJTkzJUU2JTlFJTg0X0phdmFfJUU2JTk1JUIwJUU3JUJCJTg0LyVFNSU4OCVBMCVFOSU5OSVBNCVFNiU5NSVCMCVFNyVCQiU4NCVFNiU4QyU4NyVFNSVBRSU5QSVFNCVCRCU4RCVFNyVCRCVBRSVFNSU4NSU4MyVFNyVCNCVBMCVFNiVCQyU5NCVFNyVBNCVCQSVFNSU5QiVCRS5naWY)
- 程式碼實現如下:
```java
/**
* 從陣列中刪除 index 位置的元素並且返回刪除的元素
* @param index 要刪除元素的索引
* @return 返回刪除的元素
*/
public int remove(int index) {
// 檢查 index 是否合法
if (index < 0 || index >= size) {
// 丟擲一個非法引數異常表示從陣列中刪除 index 位置的元素並且返回刪除的元素失敗,因為 index 是非法值
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
// 儲存待刪除的元素,以便返回
int removeElement = data[index];
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
// 維護 size
size--;
// 返回刪除的元素
return removeElement;
}
```
- 實現了刪除指定位置的元素的方法之後,我們可以根據該方法再衍生出兩個簡單的方法:**刪除陣列中第一個元素的方法、刪除陣列中最後一個元素的方法**。實現如下:
- **刪除陣列中第一個元素:**
```java
/**
* 從陣列中刪除第一個元素並且返回刪除的元素
* @return 返回刪除的元素
*/
public int removeFirst() {
// 複用 remove 方法實現該方法
return remove(0);
}
```
- **刪除陣列中最後一個元素:**
```java
/**
* 從陣列中刪除最後一個元素並且返回刪除的元素
* @return 返回刪除的元素
*/
public int removeLast() {
// 複用 remove 方法實現該方法
return remove(size - 1);
}
```
- 還可以**根據 remove 方法結合上之前實現的 find 方法實現一個刪除指定元素 element 的方法:**
- 該方法實現邏輯為:
- 先通過 find 方法查詢這個需要刪除的元素 element,如果找的到則會返回該元素的索引,再使用該索引呼叫 remove 方法進行刪除並且返回 true。
- 如果找不到則返回 false。
- 實現如下:
```java
/**
* 從陣列中刪除元素 element
* @param element 使用者指定的要刪除的元素
* @return 如果刪除 element 成功則返回 true;否則返回 false
*/
public boolean removeElement(int element) {
// 使用 find 方法查詢該元素的索引
int index = find(element);
// 如果找到,進行刪除
if (index != -1) {
remove(index);
return true;
} else {
return false;
}
}
```
- **需要注意的是當前陣列中是可以存在重複的元素的,如果存在重複的元素,在進行以上操作後只是刪除了一個元素,並沒有完全刪除掉陣列中的所有這個元素。對於 find 方法也是如此,如果存在重複的元素,那麼查詢到的索引則是第一個查詢到的元素的索引。**
- 所以可以接著再實現一個能**刪除陣列中重複元素的方法 removeAllElement:**
- 對於該方法,實現邏輯為:
- 先使用 find 方法尋找一次使用者指定要刪除元素 element 的索引 index。
- 再使用 while 迴圈對 index 進行判斷:
- 如果 index 不等於 -1,則在迴圈中呼叫 remove 方法將第一次查詢到的索引傳進去進行刪除。
- 然後再次使用 find 方法查詢是否還有該元素再在下一次迴圈中進行判斷。
- 以此類推直到迴圈結束就可以刪除掉陣列中所有的該元素了。
- 為了判斷陣列中是否有進行過刪除操作,我使用了一個變數 i 來記錄刪除操作的次數:
- **如果 while 迴圈結束後 i 的值大於 0 則表示進行過刪除操作,此時返回 true 代表刪除元素成功,反之返回 false 代表沒有這個元素進行刪除。**
- 具體實現程式碼如下:
```java
/**
* 刪除陣列中的所有這個元素 element
* @param element 使用者指定的要刪除的元素
* @return 刪除成功返回 true;否則返回 false
*/
public boolean removeAllElement(int element) {
// 使用 find 方法查詢該元素的索引
int index = find(element);
// 用於記錄是否有刪除過元素 element
int i = 0;
// 通過 white 迴圈刪除陣列中的所有這個元素
while (index != -1) {
remove(index);
index = find(element);
i++;
}
// 有刪除過元素 element,返回 true
// 找不到元素 element 進行刪除,返回 false
return i > 0;
}
```
- 對於查詢一個元素在陣列中的所有索引的方法這裡就不再實現了,有興趣的朋友可以自行實現。
- 至此,這個類當中的基本方法都基本實現完成了,接下來要做的操作便是使用泛型對這個類進行一些改造使其更加通用,能夠存放 “任意” 資料型別的資料。
---
### 使用泛型使該類更加通用(能夠存放 “任意” 資料型別的資料)
- 我們知道對於泛型而言,是不能夠儲存基本資料型別的,但是這些基本資料型別都有相對應的包裝類,所以對於這些基本資料型別只需使用它們對應的包裝類即可。
- 對於將該類修改成泛型類非常簡單,只需要更改幾個地方即可,不過需要**注意以下幾點:**
1. 對於泛型而言,**Java 是不支援形如 data = new E[capacity]; 直接 new 一個泛型陣列的,需要繞一個彎子來實現**,如下所示:
```java
data = (E[]) new Object[capacity];
```
2. 在上面實現 contains 方法和 find 方法時,我們在其中進行了資料間的對比操作:**if (data[i] == element)**。在我們將類轉變為泛型類之後,我們需要對這個判斷做些修改,因為在使用泛型之後,我們陣列中的資料是引用物件,我們知道**引用物件之間的對比使用 equals 方法來進行比較為好**,所以做出瞭如下修改:
```java
if (data[i].equals(element)) {
...
}
```
3. 如上所述,在使用了泛型之後,陣列中的資料都是引用物件,所以在 remove 方法的實現中,對於維護 size 變數之後,我們已經知道**此時 size 的位置是可能存在之前資料的引用的**,所以此時我們可以**將 size 這個位置置為 null,讓垃圾回收可以較為快速地將這個不需要的引用回收,避免物件的遊離**。修改如下:
```java
/**
* 從陣列中刪除 index 位置的元素並且返回刪除的元素
* @param index 要刪除元素的索引
* @return 返回刪除的元素
*/
public E remove(int index) {
...
// 維護 size
size--;
// 釋放 size 處的引用,避免物件遊離
data[size] = null;
...
}
```
- 將該類轉變為泛型類的**總修改**如下所示:
```java
public cla