想說的話
寫博客的意義,是爲了分享自己學到的知識與大家共同進步。下面的內容,都是我在極客時間上學習的一門 數據結構與算法之美——王爭老師的課,如果感興趣的話,大家可以去購買,我也不是完全照搬內容,不是爲了發博客而發,也是相當於自己的學習筆記,留言區可以留下問題,我們共同探討!
開篇思考
爲什麼許多編程語言中數組都是從0開始編號?
如何實現隨機訪問
數組(Array)是一種線性表數據結構,用一組連續的內存空間,來存儲一組相同類型的數據。線性表(LinearList):數組,鏈表,隊列,棧等數據結構
由於數組是需要連續的內存空間和相同類型的數據,才能夠支持“隨機訪問”,帶來的弊端就是爲了保證數據的連續性,使得數據的增刪需要進行大量的數據搬移。
創建一個長度爲10的int類型的數組int[ ] a = new int[10],計算機給a[10]分配了一塊連續空間1000-1039,內存塊的首地址爲base_address = 1000,圖如下所示:
計算機會給每個內存單元分配一個地址,計算機通過地址來訪問內存中的數據,當計算機通過地址訪問數據時,需要通過尋址公式來計算出元素存儲的內存地址:
a[i]_address = base_address + i * data_type_size;其中data_type_size代表了數組中每個元素的大小,上圖中的int類型的數據,data_type_size就爲4個字節;
二維數組內存尋址:
對於 m * n 的數組,a [ i ][ j ] (i < m,j < n)的地址爲:
address = base_address + ( i * n + j) * type_size
數組只有按照下標進行隨機訪問的時候時間複雜度爲O(1);
低效的插入和刪除
先看看插入操作:
如果在數組的末尾插入新的數據,時間複雜度恰好爲O(1),如果在數組的開頭插入一個元素,時間複雜度就是O(n).因爲每個位置的插入概率是相同的,所以平均時間複雜度是(1+2+…n)/n=O(n)。
對於刪除操作:
如果刪除第K個位置的數據,爲了保證內存的連續性,也需要搬移數據,保證內存的連續性,其平均時間複雜度也是O(1)。實際上,在某些特定的場景下,並不需要追求數組的連續性,每次刪除操作就是對要刪除的數據做標定,可以將多次刪除操作集中在一起執行,類似於JVM中的標記-清除算法,這樣可以提高刪除的效率。
容器能否完全替代數組?
Java中對於數組類型提供了容器類-ArrayList
ArrayList 基於動態數組實現,RandomAccess
接口標識着其支持快速隨機訪問;
public class ArrayList<E> extends AbstractList<E>
implements List<E> , RandomAccess,Cloneable,java.io.Serializable
//數組的默認大小爲10
private static final int DEFAULT_CAPACITY = 10;
ArrayList的優勢是將許多對於數組的操作封裝起來,例如:數組插入、刪除時數據搬移等,還有一個優點是支持動態擴容。
添加元素時使用 ensureCapacityInternal()
方法來保證容量足夠,如果不夠時,需要使用 grow()
方法進行擴容,新容量的大小爲 oldCapacity + (oldCapacity >> 1)
,也就是舊容量的 1.5 倍。
擴容操作需要調用 Arrays.copyOf()
把原數組整個複製到新數組中,這個操作代價很高,因此最好在創建 ArrayList 對象時就指定大概的容量大小,減少擴容操作的次數。
public boolean add(E e){
ensureCapacityInternal(size+1); //increments modcount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity){
if(elementData == DEFAULT_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity){
modCount++;
if(minCapacity - elementData.length>0)
grow(minCapacity);
}
private void grow(int minCapacity){
int oldCapacity = elementData.length;
int newCapacity = oldCapacity+(oldCapacity >>1 );
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapaciy);
elementData = Arrays.copyOf(elementData,new Capacity);
}
至於Array和ArrayList的選擇,根據實際需要:
對於業務開發,直接使用容器,省時省力,即便損耗一丟丟性能,完全不影響整體性能;但是對於底層框架的開發,性能需要做到極致,數組就會優於容器。
解答開篇
從數組的內存模型來看,“下標”最爲確切的定義是“偏移(offset)”,如果a來表示數組的首地址,a[0]就是偏移量爲0的位置,a[k]就表示偏移k個type_size的位置,所以計算a[k]內存地址只需要使用上面的尋址公式即可。
參考
[1]: 極客時間-數據結構與算法之美-數組