算法筆記(二):數組

目錄

爲什麼數組要從 0 開始編號,而不是從 1 開始呢?

數組是如何實現根據下標隨機訪問數組元素?

數組和鏈表的區別?

插入操作

刪除操作


  • 數組是最基礎、最簡單的數據結構。
  • 數組用一塊連續的內存空間,來存儲相同類型的一組數據,最大的特點就是支持隨機訪問,但插入、刪除操作也因此變得比較低效,平均情況時間複雜度爲 O(n)。
  • 平時業務開發可以直接使用編程語言提供的容器類,但特別底層的開發使用數組可能會更合適。

 

爲什麼數組要從 0 開始編號,而不是從 1 開始呢?

從數組存儲的內存模型上來看,“下標”最確切的定義應該是“偏移(offset)”。從 1 開始編號,每次隨機訪問數組元素都多了一次減法運算。數組作爲非常基礎的數據結構,通過下標隨機訪問數組元素又是其非常基礎的編程操作,效率的優化就要儘可能做到極致。所以爲了減少一次減法操作,數組選擇了從 0 開始編號,而不是從 1 開始。

如果用 a 來表示數組的首地址,a[0] 就是偏移爲 0 的位置【首地址】,a[k] 就表示偏移 k 個 type_size 的位置,所以計算 a[k] 的內存地址只需要用這個公式:

a[k]_address = base_address + k * type_size

 如果數組從 1 開始計數,那我們計算數組元素 a[k] 的內存地址就會變爲:

a[k]_address = base_address + (k-1)*type_size

 

  • 數組(Array)是一種線性表數據結構。它用一組連續的內存空間,來存儲一組具有相同類型的數據。;除了數組,鏈表、隊列、棧等也是線性表結構。二叉樹、堆、圖等是非線性表,在非線性表中,數據之間並不是簡單的前後關係。
  • 連續的內存空間和相同類型的數據;由於這兩個限制,才使數組具備了隨機訪問的特性;同時讓數組的插入和刪除操作變得非常低效,

 

數組是如何實現根據下標隨機訪問數組元素?

計算機會給每個內存單元分配一個地址,並通過地址來訪問內存中的數據。當計算機需要隨機訪問數組中的某個元素時,它會首先通過下面的尋址公式,計算出該元素存儲的內存地址;其中 data_type_size 表示數組中每個元素的大小。

a[i]_address = base_address + i * data_type_size

當數組中存儲的是 int 類型數據時 data_type_size 爲 4 個字節。

 

數組和鏈表的區別?

  • 錯誤的答案:“鏈表適合插入、刪除,時間複雜度 O(1);數組適合查找,查找時間複雜度爲 O(1)”。
  • 數組是適合查找操作,但是查找的時間複雜度並不爲 O(1)。即便是排好序的數組用二分查找的時間複雜度也是 O(logn)。所以,正確的表述應該是,數組支持隨機訪問,根據下標隨機訪問的時間複雜度爲 O(1)。

插入操作

假設數組的長度爲 n,如果將一個數據插入到數組中的第 k 個位置。爲了把第 k 個位置騰出來,給新來的數據,我們需要將第 k~n 這部分的元素都順序地往後挪一位。

插入操作的時間複雜度:

  1. 如果數組中的數據是有序的:在數組的末尾插入元素爲 O(1)【最好】;在數組的開頭插入元素爲O(n)【最壞】,在每個位置插入元素的概率一樣,所以平均時間複雜度爲 (1+2+…n)/n=O(n)。
  2. 如果數組中存儲的數據並沒有任何規律: 一個簡單方法就是,直接將第 k 位的數據搬移到數組元素的最後,把新的元素直接放入第 k 個位置。在特定場景下,在第 k 個位置插入一個元素的時間複雜度就會降爲 O(1)。在快排中也會用到這個處理思想。

刪除操作

和插入類似,如果刪除數組末尾的數據,則最好情況時間複雜度爲 O(1);如果刪除開頭的數據,則最壞情況時間複雜度爲 O(n);平均情況時間複雜度也爲 O(n)。

   多次刪除集中在一起,提高刪除效率,並不一定非得追求數組中數據的連續性。

先記錄下已經刪除的數據。每次的刪除操作並不是真正地搬移數據,只是記錄數據已經被刪除。當數組沒有更多空間存儲數據時,我們再觸發執行一次真正的刪除操作,大大減少了刪除操作導致的數據搬移。【JVM 標記清除垃圾回收算法的核心思想】

數組訪問越界造成無限循環

【暫時不理解,先記下來】

寫代碼的時候一定要警惕數組越界。在 C 語言中只要不是訪問受限的內存,所有的內存空間都是可以自由訪問的。沒有規定數組訪問越界時編譯器應該如何處理。訪問數組的本質就是訪問一段連續內存,只要數組通過偏移計算得到的內存地址是可用的,那麼程序就可能不會報任何錯誤。而Java 會做越界檢查。

去查函數調用的棧楨結構細節;函數體內的局部變量存在棧上,且是連續壓棧。在Linux進程的內存佈局中,棧區在高地址空間,從高向低增長。變量i和arr在相鄰地址,且i比arr的地址大,所以arr越界正好訪問到i。當然,前提是i和arr元素同類型,否則那段代碼仍是未決行爲。

跟編譯器分配內存和字節對齊有關 數組3個元素 加上一個變量a 。4個整數剛好能滿足8字節對齊 所以i的地址恰好跟着a2後面 導致死循環。。如果數組本身有4個元素 則這裏不會出現死循環。。因爲編譯器64位操作系統下 默認會進行8字節對齊 變量i的地址就不緊跟着數組後面了。

gcc有一個編譯選項(-fno-stack-protector)用於關閉堆棧保護功能。默認情況下啓動了堆棧保護,不管i聲明在前還是在後,i都會在數組之後壓棧,只會循環4次;如果關閉堆棧保護功能,則會出現死循環。

 

在項目開發中,什麼時候適合用數組,什麼時候適合用容器呢?

Java 中的 ArrayList、C++ STL 中的 vector都是容器類。對於業務開發用容器就OK了;做底層開發如網絡框架、性能優化時,數組就會優於容器。

  1. ArrayList 最大的優勢就是可以將很多數組操作的細節封裝起來並且支持動態擴容。每次存儲空間不夠的時候,它都會將空間自動擴容爲 1.5 倍大小。擴容操作涉及的內存申請和數據搬移比較耗時;如果事先能確定需要存儲的數據大小,最好在創建 ArrayList 的時候事先指定數據大小。’
  2. ArrayList 無法存儲基本類型如 int、long,需要封裝爲 Integer、Long 類,而 Autoboxing、Unboxing 則有一定的性能消耗,所以如果特別關注性能,或者希望使用基本類型,就可以選用數組
  3.  如果數據大小事先已知,並且對數據的操作非常簡單,用不到 ArrayList 提供的大部分方法,可以直接使用數組。
  4. 當要表示多維數組時,用數組會更加直觀。比如 Object[][] array;而用容器則需定義爲:ArrayList<ArrayList> array。

 思考; 二維數組的內存尋址公式是怎樣的呢?如何理解標記清除垃圾回收算法?

  • JVM標記清除算法:

    大多數主流虛擬機採用可達性分析算法來判斷對象是否存活,在標記階段,會遍歷所有 GC ROOTS,將所有 GC ROOTS 可達的對象標記爲存活。只有當標記工作完成後,清理工作纔會開始。

    不足:1.效率問題。標記和清理效率都不高,但是當知道只有少量垃圾產生時會很高效。2.空間問題。會產生不連續的內存空間碎片。
  • 二維數組內存尋址:

    對於 m * n 的數組,a [ i ][ j ] (i < m,j < n)的地址爲:

    address = base_address + ( i * n + j) * type_size
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章