cover

Array 是你最早學會、最常用到、但最容易忽略底層代價的資料結構。

先講結論

Array 的超能力是 O(1) 隨機存取,代價是插入刪除要搬家。如果你的場景是「讀多寫少」,Array 幾乎無敵;如果你整天在中間插來刪去,那你選錯工具了。

連續記憶體:快的根本原因

為什麼 arr[i] 可以 O(1)?因為陣列在記憶體裡是一排連續的格子,每個格子大小固定。只要知道起始位址和 index,一個加法就算出目標位址——不用從頭走,不用查表。

Index:    0     1     2     3     4
        +-----+-----+-----+-----+-----+
Data:   | 10  | 20  | 30  | 40  | 50  |
        +-----+-----+-----+-----+-----+
        ↑ 起始位址 + (i × 元素大小) = 目標位址

這也是為什麼 CPU cache 特別喜歡陣列——連續記憶體代表高 cache hit rate,遍歷時快得不像話。

讀快、插入刪除痛

操作時間為什麼?
讀取 arr[i]O(1)算位址直接跳
修改 arr[i] = xO(1)同上
尾端新增O(1)*直接放最後
中間插入O(n)後面全部往後搬
中間刪除O(n)後面全部往前搬
搜尋(未排序)O(n)最差從頭找到尾

*動態陣列平均 O(1),觸發擴容時 O(n)

在 index 1 插入 15,代價一目了然:

原本:  [10, 20, 30, 40]

Step 1: 把 index 1 之後全部往後搬
       [10, __, 20, 30, 40]

Step 2: 塞進去
       [10, 15, 20, 30, 40]

搬移量和資料量成正比——這就是 O(n) 的由來。你在一萬筆資料的中間插入一筆,就得搬五千筆。

動態陣列:空間不夠就翻倍

固定大小的陣列太死板,所以有了動態陣列(Java 的 ArrayList、Python 的 list、JS 的 Array)。策略很粗暴但有效:

容量 4,已滿: [10, 20, 30, 40]

要再塞一個?
1. 開一個容量 8 的新陣列
2. 把舊資料全部複製過去
3. 放入新元素

結果: [10, 20, 30, 40, 50, __, __, __]

擴容那一次是 O(n),但平均下來每次新增還是 O(1)——這叫 amortized O(1)。你可以想成「偶爾搬一次大家,但大部分時候免費」。

不過有個反直覺的點:如果你事先知道會放 10 萬筆資料,new ArrayList<>(100000) 預設容量,可以完全避免擴容的搬移成本。很多人不知道這件事,然後在 profile 裡看到 Arrays.copyOf 吃掉效能還很困惑。

什麼時候不該用 Array?

  • 頻繁在中間插入刪除 → 考慮 Linked List
  • 資料量完全無法預估且變動劇烈 → 考慮 Linked List 或其他動態結構
  • 需要 key-value 查找 → 你要的是 Hash Table

反過來說,如果你需要隨機存取、遍歷、或資料量大致固定,Array 就是最佳解。別什麼都用 Map,拜託。


Array 是資料結構界的白飯——不起眼,但少了它什麼菜都配不起來。