辛苦了!
上次寫了記憶體管理的話題,這次延續前一篇,整理一下 計算量(Big-O 記法) 
這是在說「這段程式,資料變多之後還撐得住嗎?」的內容。
如果你有興趣,歡迎繼續看下去 
簡單來說,就是 「當資料增加時,處理時間或記憶體會以多快的速度增加」。
重點不是看 「快/慢」,而是看 「增加的方式(成長率)」。
當資料變成 10 倍時……

像這樣的 增加方式,就用 O(1)、O(n)、O(n²) 這種形式來表示,這就是 Big-O 記法。

出處:Big-O Cheat Sheet(Eric Rowell)
橫軸是「資料量(elements)」,縱軸是「處理次數(operations)」。越往右上角,代表「資料變多時會急遽變重」=很危險,一眼就能從顏色看出來。
n 代表「資料筆數」。O(n) 的意思就是「會隨著筆數成比例增加」。
由上到下依照速度由快到慢排列。
記法名稱當資料變成 10 倍時常見例子O(1)常數不變
取出陣列的第 n 個元素O(log n)對數幾乎不變把字典/名單每次砍一半來查找(二分搜尋)O(n)線性10 倍從頭把整個列表看過一次O(n log n)準線性十幾倍快速排序等排序演算法O(n²)平方100 倍
雙重迴圈做全面比對O(n²) 要特別注意。10 筆資料時只要 100 次,但 1 萬筆時就會變成 1 億次。
看到「迴圈裡面還有迴圈」,就要提高警覺。
以「從 100 人中找出特定 1 人」這個例子來思考。

Array#include? 改成 Set(O(n) → O(1))這是 Ruby 中很常見的優化方式。在迴圈裡反覆做「有沒有包含?」 時,Array 每次都要 O(n) 掃過一次,整體就會變得很重。
# ❌ 不好的例子:include? 每次都是 O(n)。整體會變成 O(n × m)
ng_words = ["spam", "ng", ...] # 1 萬筆
comments.each do |c|
next if ng_words.include?(c.body) # 每次都從頭開始做 1 萬筆的線性搜尋💥
end
# ⭕ 好的例子:改成 Set 之後 include? 是 O(1)。整體只要 O(m)
require "set"
ng_words = Set.new(["spam", "ng", ...]) # 內部是 Hash,所以可以一下子判斷
comments.each do |c|
next if ng_words.include?(c.body) # O(1) 🙆
end
Set 是「沒有重複、沒有順序的集合」。因為內部是用 Hash 實作,所以 include?(判斷是否包含)可以做到 O(1)。
如果「順序很重要」或「想保留重複值」,那就還是用 Array 才是正確選擇。
兩個列表做比對的處理。照直覺寫,很容易變成雙重迴圈,也就是 O(n²)。
# ❌ 不好的例子:users × orders 的全面比對 → O(n²)
users.each do |u|
orders.each do |o|
matched = o if o.user_id == u.id # 內層每次都一直跑💥
end
end
# ⭕ 好的例子:先轉成 Hash,就能用 O(1) 查找 → 整體 O(n)
orders_by_user = orders.group_by(&:user_id) # 先做一次 Hash 化
users.each do |u|
matched = orders_by_user[u.id] # 用 key 一次取出 🙆
end
「內層迴圈」改成「用 Hash 一次查出來」,這是很典型的技巧。
Big-O 不只可以用在 時間,也可以用在 記憶體(空間計算量)。
這也和上次的記憶體文章接起來了。
User.all … 把全部資料載入記憶體 → 空間 O(n)User.find_each(batch_size: 1000) … 每次只處理固定筆數 → 空間 O(1)(接近)實例② 的 Hash 化,雖然把 時間從 O(n²) 改善成 O(n),但也會因為 Hash 本身而多使用記憶體(空間 O(n))。
像這樣「讓時間變快,通常會讓記憶體變多」的情況很常見,也就是 時間和空間之間常常存在取捨。
include??」就已經很有幫助了