動態陣列
一排連續的格子:用索引讀任何一格都只要一步,但在中間插入就得把後面全部往後搬。
目前的區塊:容量 8,用了 5 格
- 30
- 11
- 42
- 13
- 54
- 5
- 6
- 7
這次寫入的值被搬動的格子擴容時複製過來已配置但沒用到
一塊容量固定的連續記憶體,前面幾格有資料。試試在尾端加入,直到它塞滿。
長度/容量
5 / 8
這次寫入幾格
–
尾端加入次數
0
平均每次加入寫幾格
–
亮起來的是這一步執行的程式碼
class DynamicArray { private data: number[] = new Array(1); private length = 0; get(i: number): number { return this.data[i]; } push(value: number): void { if (this.length === this.data.length) this.grow(); this.data[this.length] = value; this.length++; } insert(index: number, value: number): void { if (this.length === this.data.length) this.grow(); for (let i = this.length; i > index; i--) { this.data[i] = this.data[i - 1]; } this.data[index] = value; this.length++; } removeAt(index: number): number { const value = this.data[index]; for (let i = index; i < this.length - 1; i++) { this.data[i] = this.data[i + 1]; } this.length--; return value; } private grow(): void { const bigger = new Array(this.data.length * 2); for (let i = 0; i < this.length; i++) { bigger[i] = this.data[i]; } this.data = bigger; }}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 需要用索引直接讀第幾個,或要依序掃過全部資料。記憶體是連續的,對 CPU 快取最友善。
- 大部分加入都在尾端。容量滿了就加倍,平均下來每次加入的成本是常數。
- 資料量小(幾十、幾百個)時,就算要在中間插入,搬一小段連續記憶體通常也比其他結構快。
和其他主題的關係
語言內建的版本
Array<T> · T[]| 操作 | 寫法 | 成本 |
|---|---|---|
| 讀第 i 個 | a[1] | O(1) |
| 尾端加入 | a.push(4) | O(1) amortised |
| 尾端移除 | a.pop() | O(1) |
| 插入到 i | a.splice(1, 0, 9) | O(n) |
| 移除第 i 個 | a.splice(1, 1) | O(n) |
| 找值 | a.indexOf(3) | O(n) |
| 有沒有 | a.includes(3) | O(n) |
| 複製一段 | a.slice(1, 3) | O(k) |
| 排序(數字) | nums.sort((x, y) => x - y) | O(n log n) |
const a = [1, 2, 3];a[1]; // → 2a.push(4); // → 4a.pop(); // → 4a.splice(1, 0, 9); // → []a; // → [1, 9, 2, 3]a.splice(1, 1); // → [9]a.indexOf(3); // → 2a.includes(3); // → truea.slice(1, 3); // → [2, 3]a.at(-1); // → 3 const b = a; // the same array, not a copyconst c = [...a]; // a shallow copyb.push(5);a.length; // → 4c.length; // → 3 const nums = [10, 9, 1];[...nums].sort(); // → [1, 10, 9]nums.sort((x, y) => x - y); // → [1, 9, 10] new Array(3).fill(0); // → [0, 0, 0]Array.from("abc"); // → ["a", "b", "c"]Array.from({ length: 2 }, () => [0, 0]); // → [[0, 0], [0, 0]]每個 → 後面的結果都是實際執行這段程式碼驗證過的。
sort()不給比較函式時會把元素轉成字串再比,所以[10, 9, 1]會排成[1, 10, 9]。數字一定要寫(x, y) => x - y。shift()/unshift()(從頭拿、從頭放)要把後面全部挪一格,是 O(n)。要當佇列用,請看佇列主題。- 二維陣列別寫
new Array(3).fill([]):三格會指向同一個陣列。用Array.from({ length: 3 }, () => [])。
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 讀第 i 個 | O(1) | O(1) |
| 尾端加入 均攤 O(1):只有擴容的那一次要複製全部 | O(1) | O(n) |
| 在開頭或中間插入 | O(n) | O(n) |
| 刪除開頭或中間 | O(n) | O(n) |
| 刪除尾端 | O(1) | O(1) |
| 找某個值 | O(n) | O(n) |
空間:O(n),容量最多是元素數的兩倍
Big O 實測:n 變大時步數怎麼長
數的是:讀或寫的格子數
| Big O | n = 1,000 | n = 10,000 | n = 100,000 | n = 1,000,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 讀第 i 個 | O(1) | 1 | 1 | 1 | 1 | ×1.0 (×1.0) |
| 尾端加入(平均,含擴容) | O(1) | 2.0 | 2.6 | 2.3 | 2.0 | ×1.0 (×1.0) |
| 在開頭插入 | O(n) | 1,000 | 10,000 | 100,000 | 1,000,000 | ×1,000 (×1,000) |
| 找某個值(平均) | O(n) | 481 | 4,809 | 48,084 | 480,836 | ×999 (×1,000) |
尾端加入的平均值在 2 到 3 之間跳動,因為取決於 n 離下一次擴容多近;但不管 n 多大,都不會往上長。
和其他做法比
| 平均 | 最差的一次 | |
|---|---|---|
| 讀第 i 格 | 1 | 1 |
| 尾端加入(寫入格數) | 2.02 | 513 |
| 開頭插入(搬動格數) | 499.5 | 999 |
| 找某個值(看幾格) | 500.5 | 1,000 |
從空陣列開始做 1,000 次操作實際數出來的。尾端加入最差的那一次要複製 512 個元素,但每次擴容都加倍,平均下來每次加入只寫 2.02 格:這就是「均攤 O(1)」。
真實世界裡的它
- JavaScript 的 Array、Python 的 list、C++ 的 std::vector、Java 的 ArrayList。
- 影像的像素、音訊的取樣、GPU 上的張量:都是一大塊連續的陣列。
- 堆積和雜湊表的底層也是陣列(見上方的關聯)。
取捨與陷阱
- 在開頭插入或刪除要搬動整排:在迴圈裡反覆
shift()或insert(0, …),整段就從 O(n) 變成 O(n²)。 - 擴容的那一次很慢(要複製全部)。對延遲敏感的程式可以事先保留容量(例如 C++ 的
reserve)。 - 擴容後舊的記憶體就被釋放了:C++ 裡指向元素的指標或迭代器,在 push 之後可能變成懸空指標。