我理解的數據結構(一)—— 數組(Array)

我理解的數據結構(一)—— 數組(Array)

首先,我是一個phper,可是畢竟php是一個腳本語言,若是使用腳本語言去理解數據結構具備必定的侷限性。由於腳本語言是不須要編譯的,若是你的語法寫的不錯,可能執行起來會要比用一個更好的數據結構來的更快、更高效(在數據量不大的狀況下)。並且數據結構是脫離任何一門語言存在的。因此,下面會選用java去更深刻的理解數據結構。

注:這裏不會去過多的解釋java的語法。php

1、定義一個數組的兩種方式

  • int[] arr = new int[10];
  • int[] arr = new int[] {10, 20, 30};

2、數組基礎

  • 數組的容量在數組一開始定義的時候就固定了。
  • 數組最大的優勢:根據索引快速查詢。如:arr[2]
  • 數組最好應用於「索引有語意」的狀況下。
  • 但並不是全部有語意的索引都適用於數組:好比索引是一我的的身份證號,會開闢過大的空間,不現實。
  • 下面會討論數組「索引沒有語意」的狀況,基於java數組,二次封裝屬於咱們本身的數組類,更深刻的理解數組。

3、建立一個最基本的數組類

學習任何一個數據結構, CRUD必不可少。下面,讓咱們來一塊兒一步步完善屬於咱們本身的數組的增、刪、改、查
public class Array {

    // 數組的實際大小
    private int size;
    // 數組
    private int[] data;

    // 構造函數,根據傳入的容納量定義一個int類型的數組
    public Array(int capacity) {
        data = new int[capacity];
        size = 0;
    }

    // 重載,沒有傳入容納量,定義一個長度爲10的int類型數組
    public Array() {
        this(10);
    }

    // 數組的實際大小
    public int getSize() {
        return size;
    }

    // 數組的容納量
    public int getCapacity() {
        return data.length;
    }

    // 數組是否爲空
    public boolean isEmpty() {
        return size == 0;
    }
}

4、增

//往數組的任意位置插入
public void add(int index, int ele) {

    // 數組已滿
    if (size == data.length) {
        throw new IllegalArgumentException("add failed. arr is full");
    }

    // 插入的索引位不合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("add failed. index < 0 or index >= size");
    }

    // 從index向後的全部元素均向後賦值
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = ele;
    size++;
}

// 第一個位置插入
public void addFirst(int ele) {
    add(0, ele);
}

// 最後一個位置插入
public void addLast(int ele) {
    add(size, ele);
}

5、查和改

// 查詢index索引位置的元素
public int get(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get failed. index is illegal");
    }
    return data[index];
}

// 查詢ele元素的索引,不存在返回-1
public int find(int ele) {
    for (int i = 0; i < size; i++) {
        if (data[i] == ele) {
            return i;
        }
    }
    return  -1;
}

// 更新Index的元素
public void set(int index, int ele) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get failed. index is illegal");
    }
    data[index] = ele;
}

6、刪

// 根據索引刪除數組中的第一個ele,返回ele
public int remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove failed. index is illegal");
    }

    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }
    size--;

    return data[index];
}

// 刪除第一個元素
public int removeFirst() {
    return remove(0);
}

// 刪除最後一個
public int removeLast() {
    return remove(size - 1);
}

// 刪除指定元素
public void removeElement(int ele) {
    int index = find(ele);
    if (index != -1) {
        remove(index);
    }
}

7、包含和重寫toString

Override
public String toString() {
    StringBuffer res = new StringBuffer();
    res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
    res.append("[");

    for (int i = 0; i < size; i++) {

        res.append(data[i]);
        if (i != size - 1) {
            res.append(", ");
        }
    }
    res.append("]");
    return res.toString();
}

// 查詢數組中是否包含元素ele
public boolean contain(int ele) {
    for (int i = 0; i < size; i++) {
        if (data[i] == ele) {
            return true;
        }
    }
    return  false;
}

注:經過以上方法咱們已經建立了一個最最最最最基本的數組類(見下圖)。固然,你也能夠去添加一些本身須要的方法,例如:removeAllfindAll之類的。
類中的方法java

可是,咱們如今的數組只支持int類型,太過侷限。接下來,咱們去給咱們的數組昇華一哈~

8、使用泛型讓咱們的數組支持「任意」數據類型

首先,爲何我要在 任意這兩個字加上引號,由於java的泛型不支持基本數據類型,只能是類的對象。
可是,這並不表明若是咱們使用了泛型,就不可使用基本數據類型了,由於每個基本數據類型都有一個對應的 包裝類
使用泛型的時候,咱們只須要傳入對應的包裝類便可。

java的基本數據類型

基本數據類型 包裝類
boolean Boolean
byte Byte
char Char
short Short
int Int
long Long
float Float
double Double

因此,咱們的代碼只須要進行極小的改動便可:

public class ArrayNew<E> {
    // 數組的實際大小
    private int size;
    // 數組
    private E[] data;

    // 構造函數,根據傳入的容納量定義一個 E 類型的數組
    public ArrayNew(int capacity) {
        // 強轉
        data = (E[]) new Object[capacity];
        size = 0;
    }

    // 重載,沒有傳入容納量,定義一個長度爲10的int類型數組
    public ArrayNew() {
        this(10);
    }

    // 數組的實際大小
    public int getSize() {
        return size;
    }

    // 數組的容納量
    public int getCapacity() {
        return data.length;
    }

    // 數組是否爲空
    public boolean isEmpty() {
        return size == 0;
    }

    // 往數組的任意位置插入
    public void add(int index, E ele) {

        // 數組已滿
        if (size == data.length) {
            throw new IllegalArgumentException("add failed. arr is full");
        }

        // 插入的索引位不合法
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("add failed. index < 0 or index > size");
        }

        // 從index向後的全部元素均向後賦值
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = ele;
        size++;
    }

    // 第一個位置插入
    public void addFirst(E ele) {
        add(0, ele);
    }

    // 最後一個位置插入
    public void addLast(E ele) {
        add(size, ele);
    }

    // 查詢index索引位置的元素
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("get failed. index is illegal");
        }
        return data[index];
    }

    // 查詢ele元素的索引,不存在返回-1
    public int find(E ele) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(ele)) {
                return i;
            }
        }
        return  -1;
    }

    // 更新Index的元素
    public void set(int index, E ele) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("get failed. index is illegal");
        }
        data[index] = ele;
    }

    // 根據索引刪除數組中的第一個ele,返回ele
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("remove failed. index is illegal");
        }
        
        E result = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = (data[i]);
        }
        // 空間釋放,垃圾回收會自動回收
        data[--size] = null;

        return result;
    }

    // 刪除第一個元素
    public E removeFirst() {
        return remove(0);
    }

    // 刪除最後一個
    public E removeLast() {
        return remove(size - 1);
    }

    // 刪除指定元素
    public void removeElement(E ele) {
        int index = find(ele);
        if (index != -1) {
            remove(index);
        }
    }

    // 查詢數組中是否包含元素ele
    public boolean contain(E ele) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(ele)) {
                return true;
            }
        }
        return  false;
    }

    @Override
    public String toString() {
        StringBuffer res = new StringBuffer();
        res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
        res.append("[");

        for (int i = 0; i < size; i++) {

            res.append(data[i]);
            if (i != size - 1) {
                res.append(", ");
            }
        }
        res.append("]");
        return res.toString();
    }

}

注:建立數組時,只需ArrayNew<Student> arr = new ArrayNew<>(20);便可。數組

9、動態數組

原理:其實,動態數組的原理很是簡單,若是咱們但願咱們的數組具備可伸縮性,只須要咱們在添加或者刪除元素時判斷 size是否到達臨界。而後去建立一個新 capacity的數組,而後把舊數組的引用指向新數組便可。
因此,咱們上述代碼的改變極小,只須要改變 addremove便可。而後添加一個 resize方法。
// 往數組的任意位置插入
public void add(int index, E ele) {
    // 插入的索引位不合法
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("add failed. index < 0 or index > size");
    }

    // 若是size == data.length,數組長度已滿
    if (size == data.length) {
        resize(data.length * 2);
    }

    // 從index向後的全部元素均向後賦值
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = ele;
    size++;
}

// 根據索引刪除數組中的第一個ele,返回ele
public E remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove failed. index is illegal");
    }

    E result = data[index];
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = (data[i]);
    }
    // 空間釋放,垃圾回收會自動回收
    data[--size] = null;

    // 減少數組長度,不要浪費空間
    if (size == data.length / 2 && size != 0) {
        resize(size);
    }

    return result;
}

// 自動伸縮數組
private void resize(int newCapacity) {
    E[] newData = (E[])new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        newData[i] = data[i];
    }
    data = newData;
}

10、簡單複雜度分析咱們封裝的數組

經過上面的分析和代碼實現,咱們封裝了一個本身的數組,而且實現了一些數組 最基本的功能,包括支持增、刪、改、查、支持任意數據類型以及動態數組。那麼咱們就來分析一下咱們本身封裝數組的複雜度。
操做 複雜度
O(n)
O(n)
已知索引O(1);未知索引O(n)
已知索引O(1);未知索引O(n)

可是:在咱們的數組中,增和刪咱們都調用了resize方法,若是size < data.length,其實咱們執行addLast複雜度只是O(1)而已(removeLast同理)。因此,咱們應該怎麼去分析resize方法所帶來的複雜度呢?數據結構

11、均攤複雜度和防止複雜度的震盪

(1)均攤複雜度

讓咱們拿 來舉例
方法 複雜度
addLast(ele) O(1)
addFirst(ele) O(n)
add(index, ele) O(n/2) = O(n)
resize(newCapacity) O(n)

其實,在執行addLast的時候,咱們並非每次都會觸發resize方法,更多的時候,複雜度只是O(1)而已。
比方說:
當前的capacity = 8,而且每一次添加操做都使用addLast,第9次addLast操做,觸發resize,總共17次基本操做(resize方法會進行8次操做,addLast方法進行9次操做)。平均,每次addLast操做,進行2次基本操做(17 / 9 ≈ 2)。
假設:
capacity = nn + 1addLast,觸發resize,總共進行了2n + 1次操做,平均每次addLast操做,進行了2次基本操做。app

這樣均攤計算,時間複雜度是O(1)!ide

(2)防止複雜度的震盪

讓咱們來假設這樣一種狀況:
size == data.length時,咱們執行了 addLast方法添加一個元素,這個時候咱們須要去執行 resize方法,此時, addLast的複雜度爲 O(n)
而後,我去 removeLast,此時的 removeLast複雜度也是 O(n)
再而後,我再去執行 addLast
.
.
.

有沒有發現,在這樣一種極端狀況下,addLastremoveLast的複雜度變成了O(n),其實,這個就是複雜度的震盪函數

  • 爲何咱們會產生這種震盪?學習

    • add狀況下,咱們去擴容數組無可厚非。可是remove狀況下,咱們馬上去縮容數組就有點不合適了。
  • 怎麼去解決這種狀況?this

    • 由於咱們以前採起的措施是Eager
    • 因此,咱們採起一種Lazy的方式:當size == data.length / 2,咱們不要馬上縮容,當size == data.length / 4時,咱們纔去縮容,就能夠很好的解決這種震盪。
具體代碼以下,其實只是對 remove進行了極小的改變
public E remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove failed. index is illegal");
    }
    
    E result = data[index];
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }
    // 空間釋放,垃圾回收會自動回收
    data[--size] = null;

    // 減少數組長度,不要浪費空間,防止震盪
    if (size == data.length / 4 && data.length / 2 != 0) {
        resize(data.length / 2);
    }

    return result;
}
相關文章
相關標籤/搜索