數組最大的優點:快速查詢
數組最好應用於“索引有語義”的情況,但並非所有有語義的索引都適用於數組,當然數組也可以處理“索引沒有語義”的情況。
自己封裝一個數組類,實現一下動態數組的效果,有簡單的增刪改查的功能。
/**
* @author ymn
* @version 1.0
* @date 2020\4\3 0003 14:27
*/
public class Array<T> {
private T[] data;
//數組實際存放的數據大小
private int size;
/**
* 構造函數,傳入數組的容量capacity構造Array
* @param capacity
*/
public Array(int capacity){
data=(T[])new Object[capacity];
size=0;
}
/**
* 無參數的構造函數,默認數組的容量capacity爲10
*/
public Array(){
this(10);
}
/**
* 獲取數組中的元素個數
* @return
*/
public int getSize(){
return size;
}
/**
* 獲取數組的容量
* @return
*/
public int getCapacity(){
return data.length;
}
/**
* 返回數組是否爲空
* @return
*/
public boolean isEmpty(){
return size==0;
}
/**
* 向所有元素後添加一個新元素
* @param e
*/
public void addLast(T e){
add(size,e);
}
/**
* 向所有元素前添加一個新元素
* @param e
*/
public void addFirst(T e){
add(0,e);
}
/**
* 在第index位置插入一個新元素e
* @param index
* @param e
*/
public void add(int index,T e){
//判斷數組是否已達到最大容量
// if (size==data.length) {
// throw new IllegalArgumentException("addLast failed,array is full");
// }
//判斷index的合法性
if(index < 0||index>size){
throw new IllegalArgumentException("addLast failed,Require index>=0 and index<=size");
}
//當數組達到最大容量時,自動擴展2倍容量
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++;
}
/**
* 獲取index索引位置的元素
* @param index
* @return
*/
public T get(int index){
//判斷index參數的合法性
if (index<0 || index>=size){
throw new IllegalArgumentException("Get Failed,Index is illegal");
}
return data[index];
}
/**
* 修改index索引位置的元素爲e
* @param index
* @param e
*/
public void set(int index,T e){
//判斷index參數的合法性
if (index<0 || index>=size){
throw new IllegalArgumentException("Get Failed,Index is illegal");
}
data[index] = e;
}
/**
* 查找數組中是否有元素e
* @param e
* @return
*/
public boolean contains(T e){
for (int i=0;i<size;i++){
if (data[i].equals(e)){
return true;
}
}
return false;
}
/**
* 查找數組中元素e所在的索引,如果不存在元素e,則返回-1
* @param e
* @return
*/
public int find(T e){
for (int i=0;i<size;i++){
if (data[i].equals(e)){
return i;
}
}
return -1;
}
/**
* 從數組中刪除index位置的元素,返回刪除的元素
* @param index
* @return
*/
public T remove(int index){
//判斷index參數的合法性
if (index<0 || index>=size){
throw new IllegalArgumentException("Remove Failed,Index is illegal");
}
T ret = data[index];
for (int i=index + 1;i<size;i++){
data[i-1] = data[i];
}
size--;
data[size] = null;
//當數組中有一半空間被浪費的時候,縮減capacity
if (size == data.length / 2){
resize(data.length / 2);
}
return ret;
}
/**
* 從數組中刪除第一個元素,返回刪除的元素
* @return
*/
public T removeFirst(){
return remove(0);
}
/**
* 從數組中刪除最後一個元素,返回刪除的元素
* @return
*/
public T removeLast(){
return remove(size-1);
}
/**
* 從數組中刪除指定元素e,因爲數組中可以放重複元素,所以當數組中有重複元素時,每次只能一個
* @param e
*/
public void removeElement(T e){
int index = find(e);
if (index != -1){
remove(index);
}
}
/**
* 實現動態數組的功能
* @param newCapacity
*/
private void resize(int newCapacity){
T[] newData = (T[])new Object[newCapacity];
for (int i=0;i<size;i++){
newData[i] = data[i];
}
data = newData;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
//%d: 佔位符; \n: 換行符
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();
}
}
簡單的時間複雜度分析:
- O(1),O(n),O(lgn),O(nlgn),O(n^2)
- 大O描述的是算法的運行時間和輸入數據之間的關係
public static int sum(int[] nums){
int sum=0;
for (int num: nums){
sum += num;
}
return sum;
}
通常情況下我們稱上面的這個算法是O(n)的,在這個算法中,n是nums中的元素個數,算法和n呈線性關係。實際上這個算法的實際運行時間爲T=c1*n+c2,但是我們一般選擇忽略常數,就稱這個算法的時間複雜度是O(n)的。O爲漸進時間複雜度,描述n趨近於無窮的情況,一般來說,O(n)的時間複雜度是遠小於O(n^2)的,但是如果考慮常數的情況,當n較小時,
O(n^2)的時間複雜度是有可能小於o(n)的。
分析一下上面的那個數組類的一些方法的時間複雜度:
添加操作:總的來說是O(n)的
addLast(e): O(1) ,O(1) 表示這個方法的運行時間和數據規模是沒有關係的,也就是說不管當前數組中有多少元素,addLast方法都能在常數時間內完成。
addFirst(e): O(n)
add(index,e): 嚴格計算需要一些概率論知識O(n/2) = O(n)
resize: O(n)
刪除操作:也是O(n)的
removeLast: O(1)
removeFirst: O(n)
remove(index): O(n)
resize : O(n)
修改操作: 已知索引O(1),未知索引O(n)
set(index,e) : O(1)
查找操作:已知索引O(1),未知索引O(n)
get(index) : O(1)
contains(e) : O(n)
find(e) : O(n)
均攤複雜度和防止複雜度的震盪:
均攤複雜度:
寫一個複雜度較高的算法,這個高複雜度的算法是爲了方便其他操作。此時我們通常會將這個複雜度較高的算法和其他的操作放在一起來分析複雜度。這個複雜度較高的算法複雜度將會均攤到其他的操作中。這種複雜度分析法我們就叫做均攤複雜度分析法。
我們說計算一個算法的時間複雜度要考慮它最壞的(時間最長)情況,但不是每一次都是最壞的情況。就像執行addLast方法不一定會執行resize方法,如果把移動一次元素稱爲一個基本操作,我們可以認爲執行一次addLast方法均攤後會執行2次基本操作,這樣的話,也可以認爲addLast和removeLast的時間複雜的爲O(1)的。
防止複雜的震盪:
如果每次執行addLast和removeLast,正好在擴容和縮容的時候,即每次執行addLast和removeLast都要執行resize方法,這種情況是非常消耗性能和時間的,我們稱這種的場景爲複雜度的的震盪。
在這個場景中,我們通過在刪除元素後不要太激進(Eager)的去釋放資源,而是懶惰(Lazy)一點,當刪除的元素只有容量(capacity)的時候才縮容
/**
* 從數組中刪除index位置的元素,返回刪除的元素
* @param index
* @return
*/
public T remove(int index){
//判斷index參數的合法性
if (index<0 || index>=size){
throw new IllegalArgumentException("Remove Failed,Index is illegal");
}
T ret = data[index];
for (int i=index + 1;i<size;i++){
data[i-1] = data[i];
}
size--;
data[size] = null;
//防止複雜度的震盪
if (size == data.length / 4 && data.length / 2!=0){
resize(data.length / 2);
}
return ret;
}