🎯 什麼情境該想到我

當你想「嚴謹確認一個迴圈/演算法真的正確」,或迴圈邏輯容易出 off-by-one、難以推理時。

⚙️ 怎麼用(找出並驗證一條在迴圈中恆成立的性質)

  1. 找不變式:一句在「每次迭代開始前都成立」的陳述(例:排序中「A[0..i-1] 已排好序」)。
  2. 證三件事:
    • 初始化:第一次迭代前,不變式成立。
    • 保持:若某次迭代前成立,執行後(下次前)仍成立。
    • 終止:迴圈結束時,不變式 + 終止條件 → 推出「演算法正確」。

實務上,即使不寫形式證明,「在腦中維持一條不變式」也能讓你迴圈寫得更對、更少邊界錯誤。

★ 真實例子:Go 標準函式庫的 sort.Search

Go 1.26.4 的 sort/search.go 把不變式直接寫在註解裡:

func Search(n int, f func(int) bool) int {
	// Define f(-1) == false and f(n) == true.
	// Invariant: f(i-1) == false, f(j) == true.
	i, j := 0, n
	for i < j {
		h := int(uint(i+j) >> 1) // avoid overflow when computing h
		// i ≤ h < j
		if !f(h) {
			i = h + 1 // preserves f(i-1) == false
		} else {
			j = h // preserves f(j) == true
		}
	}
	// i == j, f(i-1) == false, and f(j) (= f(i)) == true  =>  answer is i.
	return i
}

逐一對照三個步驟:

步驟這裡的證明
初始化i=0、j=n。約定 f(-1)=false、f(n)=true,所以 f(i-1)=false、f(j)=true 成立
保持!f(h) → 設 i=h+1,則 f(i-1)=f(h)=false ✓;f(h) → 設 j=h,則 f(j)=true ✓
終止每輪 j−i 至少縮小 1(因為 i ≤ h < j),所以一定會停;停時 i==j,不變式給出 f(i-1)=false 且 f(i)=true → i 就是第一個讓 f 為真的位置

這段程式碼寫得對的原因,就是每一行賦值都在維持不變式——註解 preserves f(i-1) == false 就是「保持」的證明。

順帶一提 uint(i+j) >> 1:寫成 (i+j)/2 在 i+j 超過 int 範圍時會溢位成負數。這是二分搜尋最有名的 bug——在各種語言的標準函式庫裡潛伏多年才被發現。

★ 用不變式決定邊界:半開區間的好處

二分搜尋的 off-by-one 錯誤,幾乎都來自「沒先想清楚不變式」。先選一種區間語義,再寫程式:

語義不變式迴圈條件縮小方式
半開 [i, j)答案在 [i, j) 裡i < ji = h+1 或 j = h
閉區間 [i, j]答案在 [i, j] 裡i <= ji = h+1 或 j = h-1

混用兩種語義就會出錯(例如用 [i, j) 的初值 j = n,卻用閉區間的 j = h-1)。Go 的 slice 表示法 s[i:j] 本身就是半開區間,所以在 Go 裡一律用半開最自然。

★ 插入排序:教科書的經典例子

《演算法導論》用插入排序介紹迴圈不變式:

for i := 1; i < len(a); i++ {
	// 不變式:a[0:i] 是原本 a[0:i] 的元素,且已排序
	key := a[i]
	j := i - 1
	for j >= 0 && a[j] > key {
		a[j+1] = a[j]
		j--
	}
	a[j+1] = key
}
  • 初始化:i=1,a[0:1] 只有一個元素,當然有序。
  • 保持:內層迴圈把比 key 大的元素右移一格,把 key 放進正確位置 → a[0:i+1] 有序。
  • 終止:i = len(a),不變式說 a[0:len(a)] 有序 → 整個陣列已排序。

★ 實務用法:把不變式變成斷言

寫不出形式證明也沒關係,把不變式寫成測試時才檢查的斷言:

if debugInvariants && !slices.IsSorted(a[:i]) {
	panic(fmt.Sprintf("invariant broken at i=%d", i))
}

配合 fuzzing(go test -fuzz)跑大量隨機輸入,不變式一被破壞就會停在第一個出錯的迭代——比在最終結果上除錯快得多。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意

  • 不變式要選「強到能推出正確性、又真的每輪都成立」的那條——太弱沒用、太強不成立。

🔗 相關工具