首先,我是一個phper,可是畢竟php是一個腳本語言,若是使用腳本語言去理解數據結構具備必定的侷限性。由於腳本語言是不須要編譯的,若是你的語法寫的不錯,可能執行起來會要比用一個更好的數據結構來的更快、更高效(在數據量不大的狀況下)。並且數據結構是脫離任何一門語言存在的。因此,下面會選用java去更深刻的理解數據結構。
注:這裏不會去過多的解釋java的語法。php
int[] arr = new int[10];
int[] arr = new int[] {10, 20, 30};
arr[2]
。
學習任何一個數據結構,
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; } }
//往數組的任意位置插入 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); }
// 查詢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; }
// 根據索引刪除數組中的第一個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); } }
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; }
注:經過以上方法咱們已經建立了一個最最最最最基本的數組類(見下圖)。固然,你也能夠去添加一些本身須要的方法,例如:removeAll
、findAll
之類的。
java
可是,咱們如今的數組只支持int類型,太過侷限。接下來,咱們去給咱們的數組昇華一哈~
首先,爲何我要在 任意這兩個字加上引號,由於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);
便可。數組
原理:其實,動態數組的原理很是簡單,若是咱們但願咱們的數組具備可伸縮性,只須要咱們在添加或者刪除元素時判斷size
是否到達臨界。而後去建立一個新capacity
的數組,而後把舊數組的引用指向新數組便可。
因此,咱們上述代碼的改變極小,只須要改變add
、remove
便可。而後添加一個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; }
經過上面的分析和代碼實現,咱們封裝了一個本身的數組,而且實現了一些數組 最基本的功能,包括支持增、刪、改、查、支持任意數據類型以及動態數組。那麼咱們就來分析一下咱們本身封裝數組的複雜度。
操做 | 複雜度 |
---|---|
增 | O(n) |
刪 | O(n) |
改 | 已知索引O(1);未知索引O(n) |
查 | 已知索引O(1);未知索引O(n) |
可是:在咱們的數組中,增和刪咱們都調用了resize
方法,若是size < data.length
,其實咱們執行addLast
複雜度只是O(1)
而已(removeLast
同理)。因此,咱們應該怎麼去分析resize
方法所帶來的複雜度呢?數據結構
讓咱們拿 增 來舉例
方法 | 複雜度 |
---|---|
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 = n
, n + 1
次addLast
,觸發resize
,總共進行了2n + 1
次操做,平均每次addLast
操做,進行了2次基本操做。app
這樣均攤計算,時間複雜度是O(1)!ide
讓咱們來假設這樣一種狀況:
當size == data.length
時,咱們執行了addLast
方法添加一個元素,這個時候咱們須要去執行resize
方法,此時,addLast
的複雜度爲O(n)
。
而後,我去removeLast
,此時的removeLast
複雜度也是O(n)
。
再而後,我再去執行addLast
。
.
.
.
有沒有發現,在這樣一種極端狀況下,addLast
和removeLast
的複雜度變成了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; }