本章是課程補充教材(cppds 書中散見於 §1.9 與 §2.5):往下鑽一層, 看陣列在記憶體裡的真實長相。電腦記憶體 = 一長條可編號的位元組(byte); 陣列 = 一段連續的、等寬的格子。這個「連續+等寬」就是 O(1) 隨機存取的全部秘密。
addr(a[i]) = base + i × sizeof(元素)
Compact array:元素的值直接躺在連續格子裡(C++ int a[]、vector<int>、
甚至 string 本身就是字元的 compact array)。
Referential array:格子裡放的是指標,真正的物件散落在 heap。
想存一排長短不一的名字("Rene"、"Joseph"、"Virginia"…)又要維持 O(1) 索引,格子就必須等寬,
於是「什麼都能裝」的動態串列一律存 8 byte 的參考。代價有二:每個元素多付 8 byte 的指標稅,
以及取值都要多跳一次記憶體。
講義用原生 new[] 自己刻了一個簡化版 vector(dscpp/arraylist.hpp),
把「動態陣列到底在做什麼」攤開來看。
幾個值得停下來看的細節:解構子要 delete[](自己配置的自己收)、
operator[] 回傳參考(讀寫共用一個運算子)、insert 與 remove 都要搬移元素,
而且容量滿了 grow() 會開一個兩倍大的新陣列搬家:vector 的 push_back 底下就是這件事。
erase(idx)(照索引左移補洞)與 display()。走訪加總 10⁶ 個 int,compact 版比 referential 版快很多,主因是?
從空的 ArrayList 連續 append n 個元素(容量 8 起跳、滿了加倍)。總共搬移了大約幾個元素?
記憶體是一維的,二維陣列只是「攤平」。C++ 標準保證 row-major:
M[0][0], M[0][1], M[1][0] 在記憶體裡連續排列,印位址就能親眼看到。定位公式:
y = x + Cols × i + j(x 是起始位址、Cols 是每列的欄數)
Fortran 和 MATLAB 用 column-major(整欄放完才放下一欄);C++ 沒有內建這種佈局, 需要的話自己開一維陣列、把公式的 i、j 對調。記住一個原則就夠了:走訪順序跟佈局一致才快。
for i for j a[i][j]
是順著記憶體走(cache 友善);內外圈對調就變成每步跳 4×行數 bytes:
大矩陣可差 5~10 倍。推薦系統的「使用者×商品」矩陣 99.9% 是 0:整片存起來是浪費。 COO(座標表:三條平行陣列 row/col/val)適合建構與批次運算; DOK(dictionary of keys:map 以 (r,c) 當 key)適合隨機讀寫。
講義把 DOK 寫成一個可運算的類別:讀寫共用 operator()(const 版查不到回 0、
非 const 版會插入)、nnz() 數非零項、sparsity() 算密度,
連加法和乘法都直接在稀疏表示上做,完全不用把矩陣展開。
一個容易誤會的細節:m(i,j) = 0 寫進去的 0 也會佔一個項目,
類別不會自動把它刪掉(上面的互動格子「點一下歸零」是示範用的簡化):
講義最後給了一張圖:整個矩陣是一個「列陣列」,每列掛一條鏈結串列, 節點存(欄號、值、next 指標)。同一列的非零項照欄號串起來,逐列運算(例如稀疏矩陣相加) 變成兩條串列的合併。這是下一章鏈結串列的預告:等你學完 ch4,回頭就能自己實作這個版本。
兩個 10⁵×10⁵、各有 10⁶ 個非零項的矩陣用 DOK 相加,工作量大約跟什麼成正比?
5×6 的 double 矩陣(8B/值)。DOK 每個非零項約需 24B(key+值+map 開銷)。幾個非零以下 DOK 才省空間?
double a[100](8B/格)base = 2000。&a[13] 是?
int a[3][4] base=1000(int=4B)。&a[2][1] 是?
100×4 的陣列,students[0][0] 的位址是 0、每個元素佔 1 格、row-major。students[5][3] 的位址是?
10⁵×10⁵ 矩陣、約 10⁶ 個非零、需要頻繁「讀寫任意 (r,c)」。選哪個?
| 結構 | 空間 | 讀 (r,c)/[i] | 強項 | 弱項 |
|---|---|---|---|---|
| compact array / vector | O(n) | O(1) | cache 友善、零 overhead | 等寬元素、中間插刪 O(n) |
| referential array | O(n)+物件 | O(1)+一跳 | 元素可異質/可共享 | cache 不友善 |
| 2D row-major | O(mn) | O(1) | 整片連續、公式定位 | 走訪順序敏感 |
| 稀疏 COO | O(nnz) | O(nnz) | 建構、批次運算 | 隨機存取慢 |
| 稀疏 DOK | O(nnz) | O(log nnz)/O(1) | 隨機讀寫 | 每項 overhead 大 |