1. 程式人生 > >極客時間——資料結構與演算法(5)

極客時間——資料結構與演算法(5)

文章結構:
陣列看起來簡單基礎,但是很多人沒有理解這個資料結構的精髓。帶著為什麼陣列要從0開始編號,而不是從1開始的問題,進入主題。
1. 陣列如何實現隨機訪問
1) 陣列是一種線性資料結構,用連續的儲存空間儲存相同型別資料
I) 線性表:陣列、連結串列、佇列、棧 非線性表:樹 圖

II) 連續的記憶體空間、相同的資料,所以陣列可以隨機訪問,但對陣列進行刪除插入,為了保證陣列的連續性,就要做大量的資料搬移工作
a) 陣列如何實現下標隨機訪問。
引入陣列再記憶體種的分配圖,得出定址公式
b) 糾正陣列和連結串列的錯誤認識。陣列的查詢操作時間複雜度並不是O(1)。即便是排好的陣列,用二分查詢,時間複雜度也是O(logn)。
正確表述:陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為O(1)
2. 低效的插入和刪除
1) 插入:從最好O(1) 最壞O(n) 平均O(n)
2) 插入:陣列若無序,插入新的元素時,可以將第K個位置元素移動到陣列末尾,把心的元素,插入到第k個位置,此處複雜度為O(1)。作者舉例說明
3) 刪除:從最好O(1) 最壞O(n) 平均O(n)
4) 多次刪除集中在一起,提高刪除效率
記錄下已經被刪除的資料,每次的刪除操作並不是搬移資料,只是記錄資料已經被刪除,當陣列沒有更多的儲存空間時,再觸發一次真正的刪除操作。即JVM標記清除垃圾回收演算法。
3. 警惕陣列的訪問越界問題


a.例子中死迴圈的問題跟編譯器分配記憶體和位元組對齊有關 陣列3個元素 加上一個變數a 。4個整數剛好能滿足8位元組對齊 所以i的地址恰好跟著a2後面 導致死迴圈。。如果陣列本身有4個元素 則這裡不會出現死迴圈。。因為編譯器64位作業系統下 預設會進行8位元組對齊 變數i的地址就不緊跟著陣列後面了。

b.函式體內的區域性變數存在棧上,且是連續壓棧。在Linux程序的記憶體佈局中,棧區在高地址空間,從高向低增長。變數i和arr在相鄰地址,且i比arr的地址大,所以arr越界正好訪問到i。當然,前提是i和arr元素同類型,否則那段程式碼仍是未決行為。

用C語言迴圈越界訪問的例子說明訪問越界的bug。此例在《C陷阱與缺陷》出現過,很慚愧,看過但是現在也只有一丟丟印象。翻了下書,替作者加上一句話:如果用來編譯這段程式的編譯器按照記憶體地址遞減的方式給變數分配記憶體,那麼記憶體中的i將會被置為0,則為死迴圈永遠出不去。
4. 容器能否完全替代陣列
相比於數字,java中的ArrayList封裝了陣列的很多操作,並支援動態擴容。一旦超過村塾容量,擴容時比較耗記憶體,因為涉及到記憶體申請和資料搬移。
陣列適合的場景:
1) Java ArrayList 的使用涉及裝箱拆箱,有一定的效能損耗,如果特別管柱效能,可以考慮陣列
2) 若資料大小事先已知,並且涉及的資料操作非常簡單,可以使用陣列
3) 表示多維陣列時,陣列往往更加直觀。
4) 業務開發容器即可,底層開發,如網路框架,效能優化。選擇陣列。
5. 解答開篇問題
1) 從偏移角度理解a[0] 0為偏移量,如果從1計數,會多出K-1。增加cpu負擔。為什麼迴圈要寫成for(int i = 0;i<3;i++) 而不是for(int i = 0 ;i<=2;i++)。第一個直接就可以算出3-0 = 3 有三個資料,而後者 2-0+1個數據,多出1個加法運算,很惱火。
2) 也有一定的歷史原因

JVM標記清除演算法:
大多數主流虛擬機器採用可達性分析演算法來判斷物件是否存活,在標記階段,會遍歷所有 GC ROOTS,將所有 GC ROOTS 可達的物件標記為存活。只有當標記工作完成後,清理工作才會開始。
不足:1.效率問題。標記和清理效率都不高,但是當知道只有少量垃圾產生時會很高效。2.空間問題。會產生不連續的記憶體空間碎片。
二維陣列記憶體定址:
對於 m * n 的陣列,a [ i ][ j ] (i < m,j < n)的地址為:
address = base_address + ( i * n + j) * type_size
另外,對於陣列訪問越界造成無限迴圈,我理解是編譯器的問題,對於不同的編譯器,在記憶體分配時,會按照記憶體地址遞增或遞減的方式進行分配。老師的程式,如果是記憶體地址遞減的方式,就會造成無限迴圈。