本章內容源于筆者對極客時間《數據結構與算法之美》以下章節的學習筆記:
開篇思考題:為什么數組要從 0 開始編號,而不是從 1 開始呢?
數組的定義
數組(Array)是一種線性表數據結構。它用一組連續的內存空間,來存儲一組具有相同類型的數據。
關鍵詞1:線性表
- 線性表:就是數據排成一條線一樣的結構,每個線性表上的數據最多只有前和后兩個方向。數組、鏈表、隊列、棧都是線性表結構。
- 非線性表:與線性表對立的概念,數據之間并不是簡單的前后關系。二叉樹、堆、圖都是非線性表結構。
關鍵詞2:連續的內存空間和相同類型的數據
- 利:支持隨機訪問。
- 弊:為了保證連續性,刪除、插入操作非常低效,因為需要做大量的數據搬移工作。
隨機訪問
數組的隨機訪問要用到元素在數組中的下標,那么這是怎么實現的?我們知道計算機會給每個內存單元分配一個地址,通過地址來訪問內存中的數據,尋址公式:
// base_address為內存塊首地址
// data_type_size為數組中每個元素的大小
a[i]_address = base_address + i * data_type_size
補充:很多人在回答數組和鏈表的區別時認為數組適合查找,查找的時間復雜度為 O(1)。這種表述不準確,數組是適合查找操作,但是查找的時間復雜度并不是 O(1),即便是排好序的數組,利用二分查找,時間復雜度也是 O(logn)。準確的說法是,數組支持隨訪問,根據下標隨機訪問的時間復雜度是 O(1)。
低效的插入和刪除
插入操作
由于數組要保證內存連續性,當要在第k個位置插入一個數據,那么k~n這部分數據都要按順序往后挪一位。假如插入的位置是數組的末尾,那么數組中原來的元素無需搬移,只需要進行1次操作;假如插入的位置是數組的的首位,那么數組中原來的元素都要往后挪一位,需要操作n次。所以數組中插入元素的時間復雜:
- 最好情況時間復雜度:O(1)
- 最壞情況時間復雜度:O(n)
- 平均情況時間復雜度:(1/n)1 + (1/n)2 + ... + (1/n)*n = O(n)
特定情況:當數組只是被當做一個存儲集合,插入第k個位置時,可以先將原本第k個位置的數據搬移到數組元素的最后,再把新的元素直接放入第k個位置。這樣時間復雜度就會降為 O(1)。
刪除操作
同樣為了保證內存的連續性,數組中的刪除操作也需要數據搬移,時間復雜度同插入操作。
特定情況:實際操作并不一定非要追求數組的連續性,要刪除數組中元素時,可以先將該元素標記為已刪除,當數組沒有更多空間存儲數據時,再觸發一次真正的刪除操作,這樣就能大大減少刪除操作導致的數據搬移。
這恰恰就是JVM標記清除垃圾回收算法的核心思想。不管是軟件開發還是架構設計,總能找到算法與數據結構的影子。
數據越界
分析以下C語言代碼的運行結果:
int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i<=3; i++){
arr[i] = 0;
printf("hello world\n");
}
return 0;
}
結果是出現無限循環,一直打印“hello world”。
解析:由于for循環的邊界條件是i<=3,而不是i<3,當i增長至3時數組arr[3]訪問越界。申明變量時i和arr并列且i在前,arr長度為3,64位操作系統下默認會進行8字節對其,4個整數剛好滿足,arr[3]越界后訪問到i。arr[3]=0,也就是i=0,于是進入無限循環。
- 很多計算機病毒正是利用到了代碼中的數組越界訪問非法地址的漏洞,來攻擊系統,編寫代碼時應當警惕數組越界。
- 很多語言會做越界檢查,拋出異常。
數組與容器
很多語言針對數組類型提供了容器類,這些容器類不僅封裝了很多數組操作的細節,還支持動態擴容。例如Java中的ArrayList,每次存儲空間不夠時,會自動擴容為原來的1.5倍大小。
相對容器,何時使用數組更合適?
- 例如Java ArrayList無法存儲int、long等基本類型,需要封裝為Integer、Long類,希望使用基本類型時就可以用數組。
- 數據大小事先已知且操作簡單可以直接用數組。
- 表示多維數組時,用數組更直觀。如Object[][] array。
總結:對于業務開發,直接使用容器就足夠了,省時省力。畢竟損耗一丟丟性能,完全不會影響到系統整體的性能。但如果是做一些非常底層的開發,比如開發網絡框架,性能的優化需要做到極致,這個時候數組就會優于容器,成為首選。
解答開篇:
為什么大多數編程語言中,數組要從0開始編號,而不是從1開始呢?
答:下標其實就是元素相對數組首地址的偏移量。數組從0開始編號,a[k]表示元素的內存地址就是:
a[k]_address = base_address + k * type_size
如果數組從1開始編號,a[k]表示元素的內存地址就是:
a[k]_address = base_address + (k-1)*type_size
對比發現,如果數組編號從1開始,每次隨機訪問元素就多了一次減法運算,CPU就多了一次減法指令。另外就是歷史原因了。
思考題一:前面提到JVM,說說你所理解的標記清除垃圾回收算法。
參考回答:大多數主流虛擬機采用可達性分析算法來判斷對象是否存活,在標記階段,會遍歷所有GC ROOTS,將所有GC ROOTS可達的對象標記為存活。只有當標記工作完成后,清理工作才會開始。不足:1.效率問題。標記和清理效率都不高,但是當知道只有少量垃圾產生時會很高效。2.空間問題。會產生不連續的內存空間碎片。
思考題二:思考一下二維數組的內存尋址公式。
參考回答:對于m*n的二位數組,a[i]j的內存地址為:
address = base_address + ( i * n + j) * type_size