Java底層實現 Array 動態數組

Array動態數組


1、Array概述

  同數組不用,數組的大小在定義時已經確定,而在實際過程中數組需要根據數據量的大小自動更改數組大小。底層實現仍是數組,只是將數組進行封裝,可以實現自適應的數組Array。

涉及的所有函數方法:

2、Array數組實現思路

  數組無非涉及增、查、改、刪四種操作,查和改操作與平常數組操作相同。難點就在於增加元素和刪除元素上。對於一般數組當索引index + 1 超過數組大小是就會報錯。爲了可以根據數據大小改變數組大小,引入capacity(容器)的概念。其中capacity >= size。如圖

自定義數組

注: 實際上capacity纔是真正數組的大小,size只是capacity中存儲數據的多少。
下面分別說一下增刪改查的的實現思路

2.1、增加元素

  如果增加後 size <= capacity 則直接將數據添加到數組中,如果增加後 size > capacity 則需要對數組進行擴容操作。擴容的方式也就是擴大爲原來的2倍。

下面以在索引index=2,元素未5爲例: 主要分爲有無擴容的請況

1)增加元素-未擴容:

  1. 複製元素向後移動
  2. 插入元素到索引位置
增加元素-未擴容

2)增加元素-擴容:

  1. 擴容爲原來的2倍,將原數據複製到新數組
  2. 將索引之後的元素向後移一位
  3. 插入元素
增加元素-未擴容

  擴容的方式就是開闢一塊原capacity的大小2倍的空間,然後將原數組的數據拷貝到新開闢的數組當中。
在頭部和尾部增加元素對應 index = 0 和 index = size - 1

程序實現:

public void add(int index, E e) {
    if (size == data.length)
        resize(2 * data.length);  //開闢新空間

    if (index < 0 || index > size)
        throw new IllegalArgumentException("addLast failed, Required index < 0 || index > size");

    System.arraycopy(data, index, data, index + 1, size - index); // 向後移動一位
    data[index] = e;  // 增加元素
    size ++;  // 維護size
}

// 私有函數  開闢新空間
private void resize(int newCapacity) {
    E[] newData = (E[]) new Object[newCapacity];
    System.arraycopy(data, 0, newData, 0, size);  // 複製數組數據到新數組
    data = newData;
}

2.2、刪除元素

  增加元素對應擴容,那麼刪除元素對應縮容,當刪除元素後 size < capacity / 2的時候進行縮容。否則直接讓對應位置等於 null 即可。
下面以刪除索引爲1的元素爲例 : 主要分爲有無縮容的請況
1)刪除元素-未縮容的情況 :

  1. 將要刪除元素後面的數據向前移一位
  2. 刪除數據中最後一位的元素,索引爲size(刪除後size已經減 1)
刪除元素-未縮容

2)刪除元素-縮容的情況 :

  1. 將要刪除元素後面的數據向前移一位
  2. 刪除數據中最後一位的元素,索引爲size(刪除後size已經減 1)
  3. 進行縮容,將原數據複製到原數組大小一半的新數組中
刪除元素-縮容

程序實現 :

public E remove(int index) {
    if (index < 0 || index >= size) {  //越界判斷
        throw new IllegalArgumentException("delete failed, Required index < 0 || index >= size");
    }
    E temp = data[index];
    System.arraycopy(data, index+1, data, index, size-index-1); //向前移動一位
    size--;    //維護size的大小
    data[size] = null;  // 清空對應位置的數據
    if (size == data.length / 2 && data.length / 2 != 0)
        resize(data.length / 2);  //進行縮容操作
    return temp;  //返回被刪除的元素
}

2.3、改變元素

改變元素不涉及擴容或者縮容,沒有特別的地方,直接修改即可。
下面以set(2, 5)爲例 :

改變元素

代碼實現:

void set(int index, E e) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get failed, Required index < 0 || index >= size");
    }
    data[index] = e;
}

2.4、查找元素

  查找元素分爲三種,查找是否有這個元素,查找這個元素所在位置,根據索引返回元素這三種。
查找是否有這個元素和查找這個元素所在位置代碼實現相似,一個找到了返回 true,一個找到了返回 index。
1)查找是否有這個元素

public boolean contains(E e){
    for (int i = 0; i < size; i++) {
        if (data[i].equals(e))
            return true;
    }
    return false;
}

2)查找這個元素所在位置

public int find(E e) {
    for (int i = 0; i < size; i++) {
        if (data[i].equals(e)) 
            return i;
    }
    return -1;
}

3、時間複雜度分析

1)增加元素的複雜度
  最好的情況是向最後的位置添加元素複雜度爲 O(1) ,最壞的情況是向頭添加元素,因爲需要數據整體向後移動,複雜度爲 O(N) ,根據均攤複雜度得出複雜度爲 O(N/2) = O(N),也就是說增加元素的複雜度爲 O(N)。但是這僅僅是考慮沒有擴容的情況產生。但是對於擴容的也是遍歷一邊數組進行復制的操作,所以複雜度也爲 O(N)。
綜上所述增加操作的時間複雜度爲 O(N)

方法 複雜度
addFirst(e) O(1)
addLast(e) O(N)
add(index, e) O(N/2) = O(N)
resize(newCapacity) O(N)

2)刪除元素的複雜度
  刪除元素的時間複雜度同增加元素完全相同。一個擴容一個縮容,實現機理相似。也就是說刪除操作的時間複雜度也是 O(N) 級別。

方法 複雜度
removeFirst() O(1)
removeLast() O(N)
remove(index) O(N/2) = O(N)
resize(newCapacity) O(N)

3)修改元素複雜度
  修改操作利用了數組的優勢,支持隨機訪問。也就是可以在 O(1)的複雜度的情況下完成操作。

方法 複雜度
set(index, e) O(1)

3)修改元素複雜度
  修改操作對應三種情況,對於支持索引查找的get方法複雜爲 O(1)。對於contain方法和find方法兩者類似,需要遍歷數組,最好的情況是在最開始就找到想要查找元素,最壞的情況是想要查找的元素在末尾,根據均攤複雜度,兩者的複雜度爲 O(N)。

方法 複雜度
get(index) O(1)
contain(e) O(N)
find(e) O(N)

4、解決時間複雜度震盪的辦法

  對於時間複雜度的震盪發生在擴容和縮容的邊界。addLast()導致擴容,removeLast()導致縮容,在邊界反覆添加和刪除操作會導致時間複雜度震盪。導致時間複雜度一直維持在O(N)級別。

改變元素
  產生的原因就是因爲我們在 removeLst 的時候過於 Eager (激進),導致與 addLast() 的擴容操作相沖突,其實我們避開 / 2 和 * 2 就可以避免震盪問題,因此我在代碼中將 removeLast 的 resize 觸發改爲 / 4.
public E remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("delete failed, Required index < 0 || index >= size");
    }
    E temp = data[index];
    System.arraycopy(data, index+1, data, index, size-index-1);
    size--;
    data[size] = null;
    if (size == data.length / 4 && data.length / 2 != 0)  //解決複雜度震盪問題
        resize(data.length / 2);
    return temp;
}

5、Array具體實現函數

公有方法

getSize()    //獲取數組大小
getCapacity() //獲取數組容器
isEmpty()     //判斷是否爲空
addFirst(E e) // 向數組頭添加元素
addLast(E e)      //向數組後添加元素
add(int index, E e)  //向index索引處增加元素
get(int index)  // 獲取索引index處元素
set(int index, E e) // 修改索引index處爲e
remove(int index)  // 刪除元素,返回刪除的元素
removeFirst()   //刪除第一個元素,
removeLast()   //刪除最後一個元素
removeElement(E e)   //刪除某一個元素
removeAllElement(E e)   //刪除所有元素e
contains(E e)  //判斷數組是否包含元素e
find(E e)  // 查找元素e所在位置
toString()   //打印函數

私有方法

resize()  //重新定義數組大小

最後

更多有關數據結構與算法的精彩內容,歡迎大家查看我的主頁:曲怪曲怪

也歡迎大家關注我的微信公衆號:TeaUrn,我在那裏等你哦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章