數據結構_1:自己動手造輪子之動態數組

一、數組

關於數組,指的就是一組有限的相關類型的變量集合。在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 的新數組


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