詳細分析動態數組的數據結構的實現過程(Java 實現)


動態數組的數據結構的實現過程(Java 實現)

數組基礎簡單回顧

  1. 數組是一種數據結構,用來存儲同一類型值的集合。

  2. 數組就是存儲數據長度固定的容器,保證多個數據的數據類型要一致

  3. 數組是一種引用數據類型

  4. 簡單來說,數組就是把需要存儲的數據排成一排進行存放。

  5. 數組的索引從 0 開始計數,最后一個位置的索引是數組的長度(n) - 1(即 n - 1

  6. 可以借助數組的索引來存取數據。

  7. 數組的索引可以有語意,也可以沒有語意

    例如,一個存儲學生成績的數組如果索引有語意,那么索引可以看成學生的學號,此時對於使用索引操作數組就可以看成對學號是 xxx 的學生進行存取成績的操作。那么如果沒有語意,就是隨意存取學生的成績到該數組中。

  8. 數組最大的優點:快速查詢。例如:array[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 類如下所示:

/**
 * 基於靜態數組封裝的數組類
 *
 * @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 的值將其加一即可。添加時也需要注意數組空間是否已經滿了。

添加過程如下圖所示:

數組末尾插入元素演示

用代碼來表示就如下所示:

/**
 * 向數組末尾添加一個新元素
 * 
 * @param element 添加的新元素
 */
public void addLast(int element) {
    // 檢查數組空間是否已滿
    if (size == data.length) {
        throw new IllegalArgumentException("向數組末尾添加元素失敗, 數組已滿!");
    }
    // 在數組末尾添加新元素
    data[size] = element;
    // 添加后維護 size 變量
    size++;
}

向數組指定索引位置添加元素

當然,也不能總是往數組末尾添加元素,當用戶有往指定索引位置添加元素的需求時,也要將其實現。

對於往指定索引位置添加元素,首先需要做的便是將該索引位置及其后面所有的元素都往后面移一個位置,將這個索引位置空出來。

需要注意:並不是真的空出來,這個位置如果之前有元素的話還是存在原來的元素,只不過是為原來元素制作了一個副本並將其往后移動了一個位置。

其次再將元素添加到該索引位置。

如果這個位置之前有元素的話實質上就是將新元素覆蓋到原來的元素上。

最后再維護存儲數組當前元素個數的變量 size 將其值加一。

當然在插入的時候也要確認數組是否有足夠的空間以及確認插入的索引位置是否合法(該位置的合法值應該為 0 到 size 這個范圍)。

具體過程如下圖所示:

往數組指定位置添加元素演示

用代碼來表示該過程就如下所示:

/**
 * 在數組的 index 索引處插入一個新元素 element
 * 
 * @param index 要插入元素的索引
 * @param element 要插入的新元素
 */
public void add(int index, int element) {
    // 檢查數組空間是否已滿
    if (size == data.length) {
        throw new IllegalArgumentException("向數組指定索引位置添加元素失敗, 數組已滿!");
    }

    // 檢查 index 是否合法
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("向數組指定索引位置添加元素失敗, index 應在 [0, 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 傳進去即可。如下所示:

/**
 * 向數組末尾添加一個新元素
 * 
 * @param element 添加的新元素
 */
public void addLast(int element) {
	// 復用 add 方法實現該方法
    add(size, element);
}

同理,也可再依此實現一個方法實現往數組首部添加一個新元素,如下所示:

/**
 * 在數組首部添加一個新元素
 * 
 * @param element 添加的新元素
 */
public void addFirst(int element) {
	// 復用 add 方法實現該方法
    add(0, element);
}

至此,對於添加操作的基本實現,已經編寫完成,接下來就接着實現在數組中查詢元素修改元素這兩個操作。

在數組中查詢元素和修改元素

查詢元素時我們需要直觀地知道數組中的信息,所以在查詢元素和修改元素之前需要先重寫 toString 方法,以讓后面我們可以直觀地看到數組中的信息,實現如下:

/**
 * 重寫 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 的合法性檢測。那么對於用戶而言,對於數組中未使用的空間,他們是永遠訪問不到的,這保證了數據的安全,他們只需知道數組中已使用的空間中的元素能夠進行訪問即可。

具體代碼實現如下:

/**
 * 獲取 index 索引位置的元素
 * 
 * @param index 要獲取元素的索引位置
 * @return 返回用戶指定的索引位置處的元素
 */
public int get(int index) {
    // 檢查 index 是否合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("獲取 index 索引位置的元素失敗, index 應在 [0, size) 范圍中!");
    }

    // 返回用戶指定的索引位置處的元素
    return data[index];
}

同理,可以實現修改元素的方法如下:

/**
* 修改 index 索引位置的元素為 element
* 
* @param index 用戶指定的索引位置
* @param element 要放到 index 處的元素
*/
public void set(int index, int element) {
    // 檢查 index 是否合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("修改 index 索引位置的元素為指定元素失敗, index 應在 [0, size) 范圍中!");
    }

    // 修改 index 索引位置的元素為 element
    data[index] = element;
}

該方法實現的內容則是修改指定位置的老元素為新元素,同樣也進行了 index 的合法性檢測,對於用戶而言是修改不了數組的那些未使用的空間的。

實現了以上方法,就可以接着實現數組中的包含、搜索和刪除這些方法了。

數組中的包含、搜索和刪除元素

在很多時候,我們在數組中存儲了許多元素,有時需要知道這些元素中是否包含了某個元素,這時候就要實現一個方法來判斷數組中是否包含我們需要的元素了。

對於該方法,實現起來也很容易,只需遍歷整個數組,逐一判斷是否包含有需要的元素即可,實現如下:

/**
 * 查找數組中是否有元素 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 表示沒有找到,實現如下:

/**
 * 查找數組中元素 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 這個位置的元素用戶是取不到的。綜上該位置如含有之前的元素是不影響接下來的操作的。

刪除指定位置元素的具體過程圖示如下:

刪除數組指定位置元素演示

代碼實現如下:

/**
 * 從數組中刪除 index 位置的元素並且返回刪除的元素
 * 
 * @param index 要刪除元素的索引
 * @return 返回刪除的元素
 */
public int remove(int index) {
    // 檢查 index 是否合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("刪除指定位置的元素失敗, index 應在 [0, size) 范圍中!");
    }

    // 存儲待刪除的元素,以便返回
    int removeElement = data[index];

    // 進行刪除
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }

    // 維護 size
    size--;

    // 返回刪除的元素
    return removeElement;
}

實現了刪除指定位置的元素的方法之后,我們可以根據該方法再衍生出兩個簡單的方法:刪除數組中第一個元素的方法、刪除數組中最后一個元素的方法。實現如下:

刪除數組中第一個元素:

/**
 * 從數組中刪除第一個元素並且返回刪除的元素
 * 
 * @return 返回刪除的元素
 */
public int removeFirst() {
    // 復用 remove 方法實現該方法
    return remove(0);
}

刪除數組中最后一個元素:

/**
 * 從數組中刪除最后一個元素並且返回刪除的元素
 * 
 * @return 返回刪除的元素
 */
public int removeLast() {
    // 復用 remove 方法實現該方法
    return remove(size - 1);
}

還可以根據 remove 方法結合上之前實現的 find 方法實現一個刪除指定元素 element 的方法:

該方法實現邏輯為:

先通過 find 方法查找這個需要刪除的元素 element,如果找的到則會返回該元素的索引,再使用該索引調用 remove 方法進行刪除並且返回 true, 如果找不到則返回 false

實現如下:

/**
 * 從數組中刪除元素 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:

對於該方法,實現邏輯為:

  1. 先使用 find 方法尋找一次用戶指定要刪除元素 element 的索引 index

  2. 再使用 while 循環對 index 進行判斷:

  3. 如果 index 不等於 -1,則在循環中調用 remove 方法將第一次查找到的索引傳進去進行刪除。

  4. 然后再次使用 find 方法查找是否還有該元素再在下一次循環中進行判斷。

  5. 以此類推直到循環結束就可以刪除掉數組中所有的該元素了。

為了判斷數組中是否有進行過刪除操作,我使用了一個變量 i 來記錄刪除操作的次數:

如果 while 循環結束后 i 的值大於 0 則表示進行過刪除操作,此時返回 true 代表刪除元素成功,反之返回 false 代表沒有這個元素進行刪除。

具體實現代碼如下:

/**
 * 刪除數組中的所有這個元素 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 一個泛型數組的,需要繞一個彎子來實現,如下所示:

    data = (E[]) new Object[capacity];
    
  2. 在上面實現 contains 方法和 find 方法時,我們在其中進行了數據間的對比操作:if (data[i] == element)。在我們將類轉變為泛型類之后,我們需要對這個判斷做些修改,因為在使用泛型之后,我們數組中的數據是引用對象,我們知道引用對象之間的對比使用 equals 方法來進行比較為好,所以做出了如下修改:

    if (data[i].equals(element)) {
        ...
    }
    
  3. 如上所述,在使用了泛型之后,數組中的數據都是引用對象,所以在 remove 方法的實現中,對於維護 size 變量之后,我們已經知道此時 size 的位置是可能存在之前數據的引用的,所以此時我們可以size 這個位置置為 null,讓垃圾回收可以較為快速地將這個不需要的引用回收,避免對象的游離。修改如下:

    /**
    * 從數組中刪除 index 位置的元素並且返回刪除的元素
    * 
    * @param index 要刪除元素的索引
    * @return 返回刪除的元素
    */
    public E remove(int index) {
        ...
        
        // 維護 size
        size--;
    
        // 釋放 size 處的引用,避免對象游離
        data[size] = null;
        
        ...
    }
    

綜上, 將該類轉變為泛型類的總修改如下所示:

public class Array<E> {

    /**
     * 靜態數組 data,基於該數組進行封裝該數組類
     * data 的長度對應其容量
     */
    private E[] data;

    /**
     * 數組當前擁有的元素個數
     */
    private int size;

    /**
     * 默認構造函數,用戶不知道要創建多少容量的數組時使用
     */
    public Array() {
        // 默認創建容量為 10 的數組
        this(10);
    }

    /**
     * 構造函數,傳入數組的容量 capacity 構造 Array
     * 
     * @param capacity 需要開辟的數組容量,由用戶指定
     */
    public Array(int capacity) {
        // 初始化 data
        data = (E[]) new Object[capacity];
        size = 0;
    }

    /**
     * 獲得數組當前的元素個數
     * 
     * @return 返回數組當前的元素個數
     */
    public int getSize() {
        return size;
    }

    /**
     * 獲得數組的容量
     * 
     * @return 返回數組的容量
     */
    public int getCapacity() {
        return data.length;
    }

    /**
     * 判斷數組是否為空
     * 
     * @return 數組為空返回 true;否則返回 false
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 在數組的 index 索引處插入一個新元素 element
     * 
     * @param index 要插入元素的索引
     * @param element 要插入的新元素
     */
    public void add(int index, E element) {
        // 檢查數組空間是否已滿
        if (size == data.length) {
            throw new IllegalArgumentException("向數組指定索引位置添加元素失敗, 數組已滿!");
        }
    
        // 檢查 index 是否合法
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("向數組指定索引位置添加元素失敗, index 應在 [0, size] 范圍中!");
        }
    
        // 將 index 及其后面所有的元素都往后面移一個位置
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }

        // 將新元素 element 添加到 index 位置
        data[index] = element;
        
        // 維護 size 變量
        size++;
    }

    /**
     * 在數組首部添加一個新元素
     * 
     * @param element 添加的新元素
     */
    public void addFirst(E element) {
        // 復用 add 方法實現該方法
        add(0, element);
    }

    /**
     * 向數組末尾添加一個新元素
     * 
     * @param element 添加的新元素
     */
    public void addLast(E element) {
        // 復用 add 方法實現該方法
        add(size, element);
    }

    /**
     * 獲取 index 索引位置的元素
     * 
     * @param index 要獲取元素的索引位置
     * @return 返回用戶指定的索引位置處的元素
     */
    public E get(int index) {
        // 檢查 index 是否合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("獲取 index 索引位置的元素失敗, index 應在 [0, size) 范圍中!");
        }
        
        // 返回用戶指定的索引位置處的元素
        return data[index];
    }

    /**
     * 修改 index 索引位置的元素為 element
     * 
     * @param index 用戶指定的索引位置
     * @param element 要放到 index 處的元素
     */
    public void set(int index, E element) {
        // 檢查 index 是否合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("修改 index 索引位置的元素為指定元素失敗, index 應在 [0, size) 范圍中!");
        }

        // 修改 index 索引位置的元素為 element
        data[index] = element;
    }

    /**
     * 查找數組中是否有元素 element
     * 
     * @param element 用戶需要知道是否存在於數組中的元素
     * @return 如果數組中包含有 element 則返回 true;否則返回 false
     */
    public boolean contains(E element) {
        // 遍歷數組,逐一判斷
        for (int i = 0; i < size; i++) {
            if (data[i].equals(element)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 查找數組中元素 element 所在的索引
     * 
     * @param element 進行搜索的元素
     * @return 如果元素 element 存在則返回其索引;不存在則返回 -1
     */
    public int find(E element) {
        // 遍歷數組,逐一判斷
        for (int i = 0; i < size; i++) {
            if (data[i].equals(element)) {
                return i;
            }
        }

        return -1;
    }

    /**
     * 從數組中刪除 index 位置的元素並且返回刪除的元素
     * 
     * @param index 要刪除元素的索引
     * @return 返回刪除的元素
     */
    public E remove(int index) {
        // 檢查 index 是否合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("刪除指定位置的元素失敗, index 應在 [0, size) 范圍中!");
        }

        // 存儲待刪除的元素,以便返回
        E removeElement = data[index];

        // 進行刪除
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }

        // 維護 size
        size--;

        // 釋放 size 處的引用,避免對象游離
        data[size] = null;

        // 返回刪除的元素
        return removeElement;
    }

    /**
     * 從數組中刪除第一個元素並且返回刪除的元素
     * 
     * @return 返回刪除的元素
     */
    public E removeFirst() {
        // 復用 remove 方法實現該方法
        return remove(0);
    }

    /**
     * 從數組中刪除最后一個元素並且返回刪除的元素
     * 
     * @return 返回刪除的元素
     */
    public E removeLast() {
        // 復用 remove 方法實現該方法
        return remove(size - 1);
    }

    /**
     * 從數組中刪除元素 element
     * 
     * @param element 用戶指定的要刪除的元素
     * @return 如果刪除 element 成功則返回 true;否則返回 false
     */
    public boolean removeElement(E element) {
        // 使用 find 方法查找該元素的索引
        int index = find(element);

        // 如果找到,進行刪除
        if (index != -1) {
            remove(index);
            return true;
        } else {
            return false;
        }
    }

    /**
     * 刪除數組中的所有這個元素 element
     * 
     * @param element 用戶指定的要刪除的元素
     * @return 刪除成功返回 true;否則返回 false
     */
    public boolean removeAllElement(E 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;
    }

    /**
     * 重寫 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();
    }
}

此時可以做一些測試:

測試代碼:

public static void main(String[] args) {
    Array<Integer> array = new Array<>(20);

    for (int i = 0; i < 10; i++) {
        array.addLast(i);
    }

    System.out.println(array + "\n");

    array.add(1, 20);
    System.out.println(array);

    array.addFirst(35);
    System.out.println(array);

    array.addLast(40);
    System.out.println(array + "\n");

    Integer e = array.remove(6);
    System.out.println("e: " + e);
    System.out.println(array + "\n");

    e = array.removeLast();
    System.out.println("e: " + e);
    System.out.println(array + "\n");

    e = array.removeFirst();
    System.out.println("e: " + e);
    System.out.println(array + "\n");

    int size = array.getSize();
    int capacity = array.getCapacity();
    System.out.println("size: " + size + ", capacity: " + capacity + "\n");

    e = array.get(3);
    System.out.println("e: " + e);

    array.set(3, 66);
    e = array.get(3);
    System.out.println("e: " + e);
    System.out.println(array + "\n");

    boolean empty = array.isEmpty();
    System.out.println("empty: " + empty);

    boolean contains = array.contains(9);
    System.out.println("contains: " + contains + "\n");

    int index = array.find(9);
    System.out.println(array);
    System.out.println("index: " + index + "\n");

    boolean b = array.removeElement(9);
    System.out.println(array);
    System.out.println("b: " + b + "\n");

    array.addLast(88);
    array.addLast(88);
    array.addLast(88);
    System.out.println(array);

    b = array.removeAllElement(88);
    System.out.println(array);
    System.out.println("b: " + b);
}

測試結果:

Array: size = 10, capacity = 20
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Array: size = 11, capacity = 20
[0, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Array: size = 12, capacity = 20
[35, 0, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Array: size = 13, capacity = 20
[35, 0, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 40]

e: 4
Array: size = 12, capacity = 20
[35, 0, 20, 1, 2, 3, 5, 6, 7, 8, 9, 40]

e: 40
Array: size = 11, capacity = 20
[35, 0, 20, 1, 2, 3, 5, 6, 7, 8, 9]

e: 35
Array: size = 10, capacity = 20
[0, 20, 1, 2, 3, 5, 6, 7, 8, 9]

size: 10, capacity: 20

e: 2
e: 66
Array: size = 10, capacity = 20
[0, 20, 1, 66, 3, 5, 6, 7, 8, 9]

empty: false
contains: true

Array: size = 10, capacity = 20
[0, 20, 1, 66, 3, 5, 6, 7, 8, 9]
index: 9

Array: size = 9, capacity = 20
[0, 20, 1, 66, 3, 5, 6, 7, 8]
b: true

Array: size = 12, capacity = 20
[0, 20, 1, 66, 3, 5, 6, 7, 8, 88, 88, 88]
Array: size = 9, capacity = 20
[0, 20, 1, 66, 3, 5, 6, 7, 8]
b: true

進程已結束,退出代碼 0

在將這個類轉換為泛型類以支持存儲 “任意” 類型的數據之后,還可以對這個類進行一些修改,使其能夠根據存儲的數據量動態地擴展以及縮小自身的空間以節約資源。

升級為動態數組

對於動態數組,我們需要實現的效果為使其能夠根據自身數據量的大小自動伸縮自身的空間,所以就相對應着兩種情況:當數組空間滿的時候進行擴容、當數組空間少到一定程度時進行減容。接下來一一實現。

當數組空間滿的時候進行擴容

對於這種情況,在我們先前的實現中,在數組空間用完時我們往其中添加新數據我們是不能再往數組中添加的,所以此時我們需要在 add 方法中做擴容操作以使能夠添加新數據進去。

對於擴容操作,可以實現一個 更改容量的方法 resize 來實現:

  1. 先構造一個容量為當前數組兩倍的新數組 newData

    • 對於為何擴容原來空間的兩倍而不是擴容一個常數,是因為如果擴容一個常數不知道要擴容多少空間。

    • 比如原先已有幾萬個元素此時擴容幾十個容量那是十分低效的,因為如果要再添加很多數據需要擴容很多次。

    • 又比如一次擴容很多容量又顯得十分浪費,比如原有 10 個數據此時擴容 1000 個容量那么可能會有很多空間會被浪費。

    • 而對於擴容為原來容量的二倍(也可以擴容為其他倍數,如 1.5 倍),是和當前數組有多少容量是相關的,擴容的量和已有的容量是一個數量級的,比如原有容量為 100 那么擴容成 200,原有容量為 10000 那么擴容為 20000,這樣子擴容是比較有優勢的,之后會進行復雜度分析分析其中的優勢。

  2. 使用循環將當前數組的數據一一復制到新數組中。

  3. 將當前數組的引用變量 data 引用到 newData 上。

  4. 對於 size 的操作依然還是之前 add 方法中的操作,不用在擴容方法中進行操作。

  5. 對於 data 之前的引用,因為此時 data 已經引用到了新數組上,沒有其他變量引用它們,所以原來的引用會被垃圾回收自動回收掉。

  6. 對於 newData 這個變量由於它是局部變量在執行完添加數據這個方法之后會自動消失,不用對其進行額外的操作。

  7. 所以最后 data 這個變量引用的就是數組擴容后並添加了新數據后的所有數據

以上過程圖示如下:

數組擴容演示

修改過后的代碼如下所示:

/**
 * 在數組的 index 索引處插入一個新元素 element
 * 
 * @param index 要插入元素的索引
 * @param element 要插入的新元素
 */
public void add(int index, E element) {
    // 檢查 index 是否合法
    if (index < 0 || index > size) {
       throw new IllegalArgumentException("向數組指定索引位置添加元素失敗, index 應在 [0, size] 范圍中!");
    }

    // 檢查數組空間是否已滿,如果已滿進行擴容,再進行添加數據的操作
    if (size == data.length) {
        // 對 data 進行擴容,擴容為原先容量的兩倍
        resize(2 * data.length);
    }

    // 將 index 及其后面所有的元素都往后面移一個位置
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }

    // 將新元素 element 添加到 index 位置
    data[index] = element;

    // 維護 size 變量
    size++;
}

/**
 * 更改 data 的容量
 * 
 * @param newCapacity data 的新容量
 */
private void resize(int newCapacity) {
    E[] newData = (E[]) new Object[newCapacity];

    for (int i = 0; i < size; i++) {
        newData[i] = data[i];
    }

    data = newData;
}

當數組空間少到一定程度時進行減容

對於這種情況,在先前的 remove 方法實現中,刪除了元素之后是沒有進行別的操作的,此時我們需要進行一個判斷,判斷數組在刪除元素后此時剩余的元素個數是否達到了一個比較小的值,如果達到我們就進行減容操作。此時先將這個值設定為數組原來容量的二分之一,如果剩余的元素個數等於這個值,這里先暫時將數組的容量減小一半。

這時候就可以復用上面實現的更改數組容量的方法了,具體代碼實現如下:

/**
 * 從數組中刪除 index 位置的元素並且返回刪除的元素
 * 
 * @param index 要刪除元素的索引
 * @return 返回刪除的元素
 */
public E remove(int index) {
    // 檢查 index 是否合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("刪除指定位置的元素失敗, index 應在 [0, size) 范圍中!");
    }

    // 存儲待刪除的元素,以便返回
    E removeElement = data[index];

    // 進行刪除
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }

    // 維護 size
    size--;

    // 釋放 size 處的引用,避免對象游離
    data[size] = null;

    // 判斷當前 data 中的元素個數是否達到了該進行減容操作的個數,如果達到進行減容
    if (size == data.length / 2) {
        // 減容操作,減小容量為原先的二分之一
        resize(data.length / 2);
    }

    // 返回刪除的元素
    return removeElement;
}

至此,已經基本實現了動態數組該具有的功能,接着對當前實現的方法進行一些簡單的時間復雜度分析以找到一些還能提升效率的地方進行修改使這個數組類更加完善。

簡單的時間復雜度分析與一些改進

對於添加操作的時間復雜度分析

對於添加操作,已經實現了三個方法,分別是:addLastaddFirstadd 方法。接下來一一簡單地分析一下它們的時間復雜度:

addLast:對於這個方法,每一次添加都是在數組末尾直接賦值,不需要移動元素,所以可以得出該方法的時間復雜度為 O(1)

addFirst:對於該方法,每一次添加都需要把數組所有元素往后移動一個位置以騰出第一個位置來放置新元素,可以得出該方法的時間復雜度為 O(n)

add:對於該方法,可能有時在數組較前面添加、可能有時在數組較后面添加,但綜合而言,移動元素的次數大約為 n/2,所以該方法的時間復雜度為 O(n/2) = O(n)

所以總的來說,添加操作的時間復雜度為 O(n)。(最壞情況)

對於添加操作中的 resize 方法,每一次執行都會復制一次數組中的所有元素,所以該方法的時間復雜度為 O(n)

對於刪除操作的時間復雜度分析

由上面的添加操作的時間復雜度分析可以很快的得出刪除操作的時間復雜度如下:

removeLastO(1)

removeFirstO(n)

removeO(n/2) = O(n)

總的來說,刪除操作的時間復雜度為 O(n)。(最壞情況)

其中的 resize 方法上面已經分析過,時間復雜度為 O(n)

對於修改操作的時間復雜度分析

對於修改操作而言,實現了 set 方法,對於該方法存在兩種情況:

知道要修改元素的索引:如果知道索引,那么可以瞬間找出要修改的元素並修改為新元素,所以時間復雜度為 O(1)

不知道要修改元素的索引:如果不知道索引,可以借助 find 方法找到索引位置再進行修改,所以這種情況需要先找后改,時間復雜度為 O(n)

對於查找操作的時間復雜度分析

對於查詢操作,實現了三個方法 getcontainsfind

get:該方法為使用索引獲取元素,時間復雜度為 O(1)

contains:該方法是一一判斷元素是否存在,時間復雜度為 O(n)

find:該方法是一一判斷元素是否存在找到其位置,時間復雜度為 O(n)

總的來說,如果知道索引,查找操作時間復雜度為 O(1);如果不知道索引,時間復雜度為 O(n)。

此時再着重觀察一下添加和刪除操作,如果我們總是只對最后一個元素進行操作(addLastremoveLast),那么此時時間復雜度是否還是為 O(n)?resize 方法是否會影響?

可以進行一些簡單的分析:

  1. 首先先看 resize 方法,對於這個方法,是不是在每一次添加或刪除元素時會影響到數組的性能呢?很顯然不是的,對於 reszie 而言並不是每次執行添加和刪除操作時都會觸發它。

  2. 比如一個數組初始容量為 10,那么它要執行 10 次添加操作才會執行一次 resize 方法,此時容量為 20,這時要再執行 10 次添加操作才會再執行 resize 方法,然后容量變為 40,這時需要執行 20 次添加操作才會再一次執行 resize 方法。

  3. 即正常情況下,需要執行 n 次添加操作才會觸發一次 resize 方法。

接着進行如下分析:

  1. 假設數組當前容量為 10,並且每一次添加操作都使用 addLast

  2. 前十次添加是沒有任何問題的,進行了 10 次 addLast 操作。

  3. 在第十一次添加時,觸發了一次 resize 方法,此時復制 10 個元素,進行了 10 次基本操作。

  4. 執行完 resize 方法之后,添加了第十一個元素,此時又進行了一次 addLast 操作。

  5. 所以到此時,一共執行了 11 次 addLast 操作,觸發了一次 resize,總共進行了 21 次基本操作。

  6. 那么平均而言,每次 addLast 操作,大約進行 2 次基本操作。時間復雜度為 O(1)

以上可歸納如下:

假設數組容量為 n,執行了 n+1 次 addLast,觸發 resize,總共進行 2n+1 次基本操作,平均每次 addLast 操作進行大約 2 次基本操作,這樣均攤計算,時間復雜度是 O(1) 的。

同理,removeLast 操作的均攤復雜度也為 O(1)。

不過此時,在我們之前的代碼實現中還存在着一個特殊情況:同時進行 addLastremoveLast 操作(復雜度震盪)。

以一個例子說明:

假設當前數組容量已滿為 n,此時進行一次 addLast 操作,那么會觸發一次 resize 方法將容量擴容為 2n,然后緊接着又執行一次 removeLast 操作,此時元素個數為 n 為容量 2n 的一半又會觸發一次 resize 方法,接着又執行一次 addLast 方法,再接着執行 removeLast 方法,以此類推,循環往復,resize 方法就會一直被觸發,每次的時間復雜度都為 O(n),這時再也不是如之前分析的那般每 n 次添加操作才會觸發一次 resize 方法了,也就是不再均攤復雜度了,這種情況也就是復雜度震盪(從預想的 O(1) 一下上升到了 O(n))。

那么此時需要進行一些改進,從上面例子可以分析出出現這種特殊情況的原因:removeLast 時觸發 resize 過於着急。

也就是當元素個數為當前容量二分之一時就進行了減容操作,將容量減少為二分之一,此時容量是滿的,這時再添加一個元素自然而然的就再一次觸發 resize 方法進行擴容了。

所以可以這樣修改:在進行 removeLast 操作時,原先實現的判斷元素個數等於容量的二分之一就進行減容的操作修改為當元素個數等於容量的四分之一時才進行減容操作,減少容量為原先的一半,這樣子減容之后,還預留了一半的空間用於添加元素,避免了以上的復雜度震盪。

所以修改代碼如下(需要注意的是在減容的過程中可能數組容量會出現等於 1 的情況,如果容量為 1,傳進 resize 方法的參數就是 1/2=0 了,這時會 new 一個空間為 0 的數組,所以需要避免這種情況):

/**
 * 從數組中刪除 index 位置的元素並且返回刪除的元素
 * 
 * @param index 要刪除元素的索引
 * @return 返回刪除的元素
 */
public E remove(int index) {
    // 檢查 index 是否合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("刪除指定位置的元素失敗, index 應在 [0, size) 范圍中!");
    }

    // 存儲待刪除的元素,以便返回
    E removeElement = data[index];

    // 進行刪除
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }

    // 維護 size
    size--;

    // 釋放 size 處的引用,避免對象游離
    data[size] = null;

    // 當 size == capacity / 4 時,進行減容操作
    if (size == data.length / 4 && data.length / 2 != 0) {
        // 減容操作,減小容量為原先的二分之一
        resize(data.length / 2);
    }

    // 返回刪除的元素
    return removeElement;
}

至此,這個數組類就封裝完成了,總的來說這個類基於一個靜態數組實現了一個支持增刪改查數據、動態更改數組空間和支持存儲 “任意” 數據類型的數據的數組數據結構。


如有寫的不足的,請見諒,請大家多多指教。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM