玩轉資料結構(02)--陣列_2
1.動態陣列(解決陣列空間不夠用的情況)
Java 中的靜態陣列,當插入的值數量 > 陣列的size 時就會報錯,使用動態陣列可以解決這個問題,
設定動態陣列的思路:再建立一個新的陣列newData,它要比之前的陣列空間大一些;將data 中的資料放入到 newData 中,迴圈遍歷陣列data中所有的元素,將他們依次賦值到 newData 中;
想讓 newData 取代原來的 data ,對陣列而言,容量(capacity)已經變為 8 ;size 在newData中還是 4 ,但陣列可以裝入更多的元素;
將陣列data的引用改為指向 新的有8個空間的陣列,與newData的引用相同,指向同樣的空間;整個過程封裝在函式中,當函式執行完成後,newData 就會失效了;而 data 是整個類的成員變數,和整個類的成員變數是相同的,只要類在使用則data就是有效的;
之前的4個空間的陣列,因為已經沒有引用了,垃圾回收器會將其銷燬;陣列完成擴容
示例程式碼:Array.java
public class Array<E> { private E[] data; private int size; // 建構函式,傳入陣列的容量capacity構造Array public Array(int capacity){ data = (E[])new Object[capacity]; size = 0; } // 無引數的建構函式,預設陣列的容量capacity=10 public Array(){ this(10); } // 獲取陣列的容量 public int getCapacity(){ return data.length; } // 獲取陣列中的元素個數 public int getSize(){ return size; } // 返回陣列是否為空 public boolean isEmpty(){ return size == 0; } // 在index索引的位置插入一個新元素e public void add(int index, E e){ if(index < 0 || index > size) throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size."); if(size == data.length) resize(2 * data.length); //(新增程式碼)進行陣列擴容,變為原來的2倍 for(int i = size - 1; i >= index ; i --) data[i + 1] = data[i]; data[index] = e; size ++; } // 向所有元素後新增一個新元素 public void addLast(E e){ add(size, e); } // 在所有元素前新增一個新元素 public void addFirst(E e){ add(0, e); } // 獲取index索引位置的元素 public E get(int index){ if(index < 0 || index >= size) throw new IllegalArgumentException("Get failed. Index is illegal."); return data[index]; } // 修改index索引位置的元素為e public void set(int index, E e){ if(index < 0 || index >= size) throw new IllegalArgumentException("Set failed. Index is illegal."); data[index] = e; } // 查詢陣列中是否有元素e public boolean contains(E e){ for(int i = 0 ; i < size ; i ++){ if(data[i].equals(e)) return true; } return false; } // 查詢陣列中元素e所在的索引,如果不存在元素e,則返回-1 public int find(E e){ for(int i = 0 ; i < size ; i ++){ if(data[i].equals(e)) return i; } return -1; } // 從陣列中刪除index位置的元素, 返回刪除的元素 public E remove(int index){ if(index < 0 || index >= size) throw new IllegalArgumentException("Remove failed. Index is illegal."); E ret = data[index]; for(int i = index + 1 ; i < size ; i ++) data[i - 1] = data[i]; size --; data[size] = null; // loitering objects != memory leak if(size == data.length / 2) //新增程式碼,當陣列中實際使用的空間為一半時[size 是 capacity 的一半] resize(data.length / 2); //使陣列變為當前設定陣列容量的一半 return ret; } // 從陣列中刪除第一個元素, 返回刪除的元素 public E removeFirst(){ return remove(0); } // 從陣列中刪除最後一個元素, 返回刪除的元素 public E removeLast(){ return remove(size - 1); } // 從陣列中刪除元素e public void removeElement(E e){ int index = find(e); if(index != -1) remove(index); } @Override public String toString(){ StringBuilder res = new StringBuilder(); 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(); } // (新增程式碼)將陣列空間的容量變成newCapacity大小(2倍) private void resize(int newCapacity){ E[] newData = (E[])new Object[newCapacity]; //new 一個新的E型陣列newData for(int i = 0 ; i < size ; i ++) newData[i] = data[i]; //將原來的陣列內容放入到新的newData中 data = newData; //讓 data 指向 newData 的空間 } }
Main.java
public class Main { public static void main(String[] args) { Array<Integer> arr = new Array<>();//修改程式碼,()中不填容量大小,使用預設容量 for(int i = 0 ; i < 10 ; i ++) arr.addLast(i); System.out.println(arr); arr.add(1, 100); System.out.println(arr); arr.addFirst(-1); System.out.println(arr); arr.remove(2); System.out.println(arr); arr.removeElement(4); System.out.println(arr); arr.removeFirst(); System.out.println(arr); } }
輸出:
2.複雜度分析
時間複雜度分析:例:O(1)、O(n)、O(lgn)、O(nlogn)、O(n^2)
O:描述的是演算法的執行時間和輸入資料之間的關係
上圖中:c1指【for 迴圈中,將nums這個數取出來、將sum這個數取出來、將sum和nums 加在一起、最後將結果扔回給sum變數】這些操作花費的總時間;實際中無法具體取得
c2指【在整個程式中,開闢 Int 型空間 sum,並將0的初始化空間賦值給 sum,在運算結束後,還有返回sum】這些操作花費的總時間;實際中無法具體取得
前兩個忽略常數,都是O(n),雖然第三個的常數值很小,但它還是O(n^2),但並不代表:對於任意輸入來說,O(n)都要優於O(n^2),O 是漸進時間複雜度,【描述 n 趨近於無窮的情況來比較演算法的效能】;最後一箇中,低階的300n 相比 n^2(高階)可忽略,故為O(n^2);
3.動態陣列的時間複雜度分析
新增操作:通常情況下(按最壞的情況看)是 O(n)
addLast(e) ---O(1) :該操作所消耗時間與資料規模沒有關係,無論陣列中有多少元素,addLast都能在常數時間內完成。
addFirst(e) ---O(n) :陣列頭新增元素需要將陣列每個元素向後移動一個單位,故為O(n)。
add(index,e) --O(n/2)=O(n) :在陣列 index 索引的位置插入元素e,時間複雜度與 index 相關,index = 0時和addFirst(e)相同;index = size時和addLast(e)相同,分析:假設多種情況下的概率相同,運用概率論的知識,求出時間的期望。平均來看需要右移動 n/2 個元素,O需要忽略常數,故為 O(n)。
刪除操作:通常情況下(按最壞的情況看)是 O(n)
removeLast(e) ---O(1)
removeFirst(e) ---O(n)
remove(index,e) --- O(n/2)=O(n)
resize ---O(n)
修改操作:已知索引:O(1) 陣列最大優勢--支援隨機訪問,只要知道索引就可以馬上訪問到該資料
未知索引:O(n) 需要從頭遍歷陣列來找到該元素進行修改
set(index,e) ---O(1)
查詢操作:需要從頭遍歷陣列來找到該元素
get(index) ---O(1) 知道所要查詢元素的索引,立馬可以拿到該值
contains(e) --- O(n) 不知道所要查詢元素的索引,檢視陣列中是否包含某個元素
find(e) ---O(n) 不知道所要查詢元素的索引,檢視陣列中 e 元素對應的索引是多少
4.均攤複雜度和防止複雜度的震盪
resize 的時間複雜度分析
因為最壞的情況(resize)要在很多次 addLast 操作後才會執行; 運用均攤複雜度比較合適;
addLast 和 removeLast 的均攤複雜度均為O(1);
複雜度震盪
上面的那種所說的是大多數情況下,都不會執行resize,現在認為製造這樣的情景,讓 addLast 和 removeLast 依次執行多次,這樣就會每次都執行 resize ,addLast 和 removeLast 的時間複雜度均為O(n);從O(1)變為O(n),產生複雜度震盪
解決複雜度震盪問題:
出現原因:removeLast 時 resize 過於著急
解決方案:設定 resize == capacity/4 時,才將 capacity 減半
圖解過程:
1.新增元素超過 size ,容量擴大一倍
2.刪除元素後,先不著急縮小陣列容量
3.等到陣列長度size縮小到陣列容量capacity 的1/4 時
4.縮小陣列容量,但也只縮小到原來的一半,仍然預留了一半的陣列容量供陣列進行 addLast 操作
示例程式碼:Array.java
public class Array<E> {
private E[] data;
private int size;
// 建構函式,傳入陣列的容量capacity構造Array
public Array(int capacity){
data = (E[])new Object[capacity];
size = 0;
}
// 無引數的建構函式,預設陣列的容量capacity=10
public Array(){
this(10);
}
// 獲取陣列的容量
public int getCapacity(){
return data.length;
}
// 獲取陣列中的元素個數
public int getSize(){
return size;
}
// 返回陣列是否為空
public boolean isEmpty(){
return size == 0;
}
// 在index索引的位置插入一個新元素e
public void add(int index, E e){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
if(size == data.length)
resize(2 * data.length);
for(int i = size - 1; i >= index ; i --)
data[i + 1] = data[i];
data[index] = e;
size ++;
}
// 向所有元素後新增一個新元素
public void addLast(E e){
add(size, e);
}
// 在所有元素前新增一個新元素
public void addFirst(E e){
add(0, e);
}
// 獲取index索引位置的元素
public E get(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Get failed. Index is illegal.");
return data[index];
}
// 修改index索引位置的元素為e
public void set(int index, E e){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Set failed. Index is illegal.");
data[index] = e;
}
// 查詢陣列中是否有元素e
public boolean contains(E e){
for(int i = 0 ; i < size ; i ++){
if(data[i].equals(e))
return true;
}
return false;
}
// 查詢陣列中元素e所在的索引,如果不存在元素e,則返回-1
public int find(E e){
for(int i = 0 ; i < size ; i ++){
if(data[i].equals(e))
return i;
}
return -1;
}
// 從陣列中刪除index位置的元素, 返回刪除的元素
public E remove(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal.");
E ret = data[index];
for(int i = index + 1 ; i < size ; i ++)
data[i - 1] = data[i];
size --;
data[size] = null; // loitering objects != memory leak
if(size == data.length / 4 && data.length / 2 != 0)
//(修改程式碼,陣列長度size為陣列容量1/4時才會縮小陣列容量)
resize(data.length / 2);
return ret;
}
// 從陣列中刪除第一個元素, 返回刪除的元素
public E removeFirst(){
return remove(0);
}
// 從陣列中刪除最後一個元素, 返回刪除的元素
public E removeLast(){
return remove(size - 1);
}
// 從陣列中刪除元素e
public void removeElement(E e){
int index = find(e);
if(index != -1)
remove(index);
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
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();
}
// 將陣列空間的容量變成newCapacity大小
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity];
for(int i = 0 ; i < size ; i ++)
newData[i] = data[i];
data = newData;
}
}