一篇文章讓你瞭解動態數組的數據結構的實現過程(Java 實現)

數組基礎簡單回顧

  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[] 進行二次封裝實現增、刪、改、查的操做。接下來將一一實現。java

  • 成員變量設計數組

    • 因爲數組自己是靜態的,在建立的時候需指定大小,此時我將這個大小用變量 capacity 表示,即容量,表示數組空間最多裝幾個元素。但並不須要在類中聲明,只需在構造函數的參數列表中聲明便可,由於數組的容量也就是 data[] 的長度,不須要再聲明一個變量來進行維護。安全

    • 對於數組中實際擁有的元素個數,這裏我用變量 size 來表示。初始時其值爲 0數據結構

      • 這個 size 也能表示爲數組中第一個沒有存放元素的位置
      • 例如數組爲空時,size 爲 0,此時索引 0 處爲數組中第一個沒有存放元素的位置;再如數組中有兩個元素時,size 爲 2,此時索引 0 和 1 處都有元素,索引 2 處沒有,也就是數組中第一個沒有存放元素的位置。
    • 因此可先建立 Array 類以下所示:app

      /**
       * 基於靜態數組封裝的數組類
       *
       * @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;
          }
      
      }

向數組中添加元素

  • 對於向數組中添加元素,向數組末尾添加元素是最簡單的,原理以下:ide

    • 顯而易見,往數組末尾添加元素是添加操做中最簡單的操做,由於咱們已經知道 size 這個變量指向的是數組第一個沒有元素的地方,很容易理解,size 這個位置就是數組末尾的位置,因此往這個位置添加元素時也就是往數組末尾添加元素了,添加後維護 size 的值將其加一便可。當前添加時也須要注意數組空間是否已經滿了。函數

    • 添加過程以下圖所示:
      數組末尾插入元素演示性能

    • 用代碼來表示就以下所示:測試

      /**
       * 向數組末尾添加一個新元素
       * @param element 添加的新元素
       */
      public void addLast(int element) {
          // 檢查數組空間是否已滿
          if (size == data.length) {
              // 拋出一個非法參數異常表示向數組末尾添加元素失敗,由於數組已滿
              throw new IllegalArgumentException("AddLast failed. Array is full.");
          }
          // 在數組末尾添加新元素
          data[size] = element;
          // 添加後維護 size 變量
          size++;
      }
  • 固然,也不能老是往數組末尾添加元素,當用戶有往指定索引位置添加元素的需求時,也要將其實現:ui

    • 對於往指定索引位置添加元素:首先須要作的即是將該索引位置及其後面全部的元素都日後面移一個位置,將這個索引位置空出來。

      • 須要注意:並非真的空出來,這個位置若是以前有元素的話仍是存在原來的元素,只不過已經爲原來元素製做了一個副本並將其日後移動了一個位置。
    • 其次再將元素添加到該索引位置。

      • 若是這個位置以前有元素的話實質上就是將新元素覆蓋到原來的元素上。
    • 最後再維護存儲數組當前元素個數的變量 size 將其值加一。

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

    • 具體過程以下圖所示:
      往數組指定位置添加元素演示

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

      /**
       * 在數組的 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 傳進去便可。以下所示:

      /**
       * 向數組末尾添加一個新元素
       * @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) {
                // 拋出一個非法參數異常表示獲取 index 索引位置的元素失敗,由於 index 是非法值
                throw new IllegalArgumentException("Get failed. Index is illegal.");
            }
        
            // 返回用戶指定的索引位置處的元素
            return data[index];
        }
  • 同理,能夠實現修改元素的方法以下:

    /**
     * 修改 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 的合法性檢測,對於用戶而言是修改不了數組的那些未使用的空間的。
  • 實現了以上方法,就能夠接着實現數組中的包含、搜索和刪除這些方法了。


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

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

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

      /**
       * 查找數組中是否有元素 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) {
              // 拋出一個非法參數異常表示從數組中刪除 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;
      }
    • 實現了刪除指定位置的元素的方法以後,咱們能夠根據該方法再衍生出兩個簡單的方法:刪除數組中第一個元素的方法、刪除數組中最後一個元素的方法。實現以下:

      • 刪除數組中第一個元素:

        /**
         * 從數組中刪除第一個元素而且返回刪除的元素
         * @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:

        • 對於該方法,實現邏輯爲:

          • 先使用 find 方法尋找一次用戶指定要刪除元素 element 的索引 index。
          • 再使用 while 循環對 index 進行判斷:
            • 若是 index 不等於 -1,則在循環中調用 remove 方法將第一次查找到的索引傳進去進行刪除。
            • 而後再次使用 find 方法查找是否還有該元素再在下一次循環中進行判斷。
            • 以此類推直到循環結束就能夠刪除掉數組中全部的該元素了。
        • 爲了判斷數組中是否有進行過刪除操做,我使用了一個變量 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("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++;
        }
    
        /**
         * 在數組首部添加一個新元素
         * @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) {
                // 拋出一個非法參數異常表示獲取 index 索引位置的元素失敗,由於 index 是非法值
                throw new IllegalArgumentException("Get failed. Index is illegal.");
            }
    
            // 返回用戶指定的索引位置處的元素
            return data[index];
        }
    
        /**
         * 修改 index 索引位置的元素爲 element
         * @param index 用戶指定的索引位置
         * @param element 要放到 index 處的元素
         */
        public void set(int index, E element) {
            // 檢查 index 是否合法
            if (index < 0 || index >= size) {
                // 拋出一個非法參數異常表示修改 index 索引位置的元素爲 element 失敗,由於 index 是非法值
                throw new IllegalArgumentException("Set failed. Index is illegal.");
            }
    
            // 修改 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) {
                // 拋出一個非法參數異常表示從數組中刪除 index 位置的元素而且返回刪除的元素失敗,由於 index 是非法值
                throw new IllegalArgumentException("Remove failed. Index is illegal.");
            }
    
            // 存儲待刪除的元素,以便返回
            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) {
              // 拋出一個非法參數異常表示向數組指定索引位置添加元素失敗,應該讓 index 在 0 到 size 這個範圍才行
              throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= 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) {
              // 拋出一個非法參數異常表示從數組中刪除 index 位置的元素而且返回刪除的元素失敗,由於 index 是非法值
              throw new IllegalArgumentException("Remove failed. Index is illegal.");
          }
      
          // 存儲待刪除的元素,以便返回
          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;
      }
  • 至此,已經基本實現了動態數組該具備的功能,接着對當前實現的方法進行一些簡單的時間複雜度分析以找到一些還能提高效率的地方進行修改使這個數組類更加完善。


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

  • 對於添加操做的時間複雜度分析

    • 對於添加操做,已經實現了三個方法,分別是:addLast、addFirst、add 方法。接下來一一簡單地分析一下它們的時間複雜度:
      • addLast:對於這個方法,每一次添加都是在數組末尾直接賦值,不須要移動元素,因此能夠得出該方法的時間複雜度爲 O(1)。
      • addFirst:對於該方法,每一次添加都須要把數組全部元素日後移動一個位置以騰出第一個位置來放置新元素,能夠得出該方法的時間複雜度爲 O(n)。
      • add:對於該方法,可能有時在數組較前面添加、可能有時在數組較後面添加,但綜合而言,移動元素的次數大約爲 n/2,因此該方法的時間複雜度爲 O(n/2) = O(n)。
      • 因此總的來講,添加操做的時間複雜度爲 O(n)。(最壞狀況)
      • 對於添加操做中的 resize 方法,每一次執行都會複製一次數組中的全部元素,因此該方法的時間複雜度爲 O(n)。
  • 對於刪除操做的時間複雜度分析

    • 由上面的添加操做的時間複雜度分析能夠很快的得出刪除操做的時間複雜度以下:
      • removeLast:O(1)
      • removeFirst:O(n)
      • remove:O(n/2) = O(n)
      • 總的來講,刪除操做的時間複雜度爲 O(n)。(最壞狀況)
      • 其中的 resize 方法上面已經分析過,時間複雜度爲 O(n)。
  • 對於修改操做的時間複雜度分析

    • 對於修改操做而言,實現了 set 方法,對於該方法存在兩種狀況:
      • 知道要修改元素的索引:若是知道索引,那麼能夠瞬間找出要修改的元素並修改成新元素,因此時間複雜度爲 O(1)。
      • 不知道要修改元素的索引:若是不知道索引,能夠藉助 find 方法找到索引位置再進行修改,因此這種狀況須要先找後改,時間複雜度爲 O(n)。
  • 對於查找操做的時間複雜度分析

    • 對於查詢操做,實現了三個方法 get、contains、find:
      • get:該方法爲使用索引獲取元素,時間複雜度爲 O(1)。
      • contains:該方法是一一判斷元素是否存在,時間複雜度爲 O(n)。
      • find:該方法是一一判斷元素是否存在找到其位置,時間複雜度爲 O(n)。
      • 總的來講,若是知道索引,查找操做時間複雜度爲 O(1);若是不知道索引,時間複雜度爲 O(n)。
  • 此時再着重觀察一下添加和刪除操做,若是咱們老是隻對最後一個元素進行操做(addLast 或 removeLast),那麼此時時間複雜度是否仍是爲 O(n)?resize 方法是否會影響?

  • 能夠進行一些簡單的分析:

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

    • 好比一個數組初始容量爲 10,那麼它要執行 10 次添加操做纔會執行一次 resize 方法,此時容量爲 20,這時要再執行 10 次添加操做纔會再執行 resize 方法,而後容量變爲 40,這時須要執行 20 次添加操做纔會再一次執行 resize 方法。

      • 即正常狀況下,須要執行 n 次添加操做纔會觸發一次 resize 方法。
    • 接着進行以下分析:

      • 假設數組當前容量爲 10,而且每一次添加操做都使用 addLast:
        • 前十次添加是沒有任何問題的,進行了 10 次 addLast 操做。
        • 在第十一次添加時,觸發了一次 resize 方法,此時複製 10 個元素,進行了 10 次基本操做。
        • 執行完 resize 方法以後,添加了第十一個元素,此時又進行了一次 addLast 操做。
        • 因此到此時,一共執行了 11 次 addLast 操做,觸發了一次 resize,總共進行了 21 次基本操做。
        • 那麼平均而言,每次 addLast 操做,大約進行 2 次基本操做。時間複雜度爲 O(1)。
        • 以上可概括以下:
          • 假設數組容量爲 n,執行了 n+1 次 addLast,觸發 resize,總共進行 2n+1 次基本操做,平均每次 addLast 操做進行大約 2 次基本操做,這樣均攤計算,時間複雜度是 O(1) 的。
    • 同理,removeLast 操做的均攤複雜度也爲 O(1)。

    • 不過此時,在咱們以前的代碼實現中還存在着一個特殊狀況:同時進行 addLast 和 removeLast 操做(複雜度震盪)。

    • 以一個例子說明:

      • 假設當前數組容量已滿爲 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) {
                // 拋出一個非法參數異常表示從數組中刪除 index 位置的元素而且返回刪除的元素失敗,由於 index 是非法值
                throw new IllegalArgumentException("Remove failed. Index is illegal.");
            }
        
            // 存儲待刪除的元素,以便返回
            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;
        }
  • 至此,這個數組類就封裝完成了,總的來講這個類基於一個靜態數組實現了一個支持增刪改查數據、動態更改數組空間和支持存儲 「任意」 數據類型的數據的數組數據結構


若有寫的不足的,請見諒,請你們多多指教。(*^▽^*)

相關文章
相關標籤/搜索