1. 定位导航
前面已经学习了算法的基本概念、运行时间分析,以及插入排序的执行过程。现在要补上另一块非常重要的内容:正确性证明。
运行时间回答的是:
text
算法快不快?
循环不变式回答的是:
text
算法为什么对?
2. 概念术语
| 术语 | 定义 | 举例 |
|---|---|---|
| 循环不变式 | 循环过程中始终保持成立的性质 | 插入排序中左侧子数组始终有序 |
| 初始化 | 循环第一次执行前,不变式成立 | 左侧只有一个元素,天然有序 |
| 保持 | 每轮循环后,不变式仍然成立 | 插入 key 后,左侧仍有序 |
| 终止 | 循环结束时,不变式推出正确结果 | 整个数组已经有序 |
| 正确性证明 | 说明算法对所有合法输入都能输出正确结果 | 不是只靠测试样例 |
关键澄清:
- 循环不变式不是循环条件。
- 循环不变式不是最终结果本身。
- 循环不变式要在每轮循环前后都能成立。
- 它的作用是把"每一步没错"连接到"最终结果正确"。
3. 什么是循环不变式
循环不变式可以理解成:
text
在循环执行过程中,一直保持为真的关键性质。
它像一条安全绳:
- 循环开始前,它成立;
- 每执行一轮,它仍然成立;
- 循环结束时,它帮助我们推出最终结论。

4. 三步证明法:初始化、保持、终止
4.1 初始化
证明循环第一次执行之前,不变式已经成立。
4.2 保持
假设某一轮循环开始前不变式成立,证明这一轮结束后,不变式仍然成立。
4.3 终止
证明循环结束时,不变式能够推出算法的正确输出。
5. 插入排序中的循环不变式
插入排序可以维护这样一个不变式:
在每轮外层循环开始前,当前下标左边的子数组已经有序,并且包含的是原数组对应前缀元素的一个重排。

这句话包含两个关键点:
第一,左边部分是有序的。
第二,左边部分没有丢元素,也没有多元素,只是顺序发生了变化。
6. 动态推演:不变式如何贯穿排序过程
下面用一个动态过程观察循环不变式如何保持。

以数组:
text
[5, 2, 4, 6, 1, 3]
为例,插入排序每一轮都把当前 key 插入到左侧有序部分的正确位置。每轮结束后,左侧有序部分会扩大一格。直到最后,整个数组都成为有序部分。
7. 用三步法证明插入排序正确
7.1 初始化
第一次外层循环开始前,左侧只有第一个元素。
一个只包含一个元素的数组,天然是有序的,所以循环不变式成立。
7.2 保持
假设某一轮开始前,左侧子数组已经有序。
这一轮会取出当前元素 key,然后从右向左扫描左侧有序部分,把所有比 key 大的元素向右移动一格。
当找到合适位置后,把 key 放进去。因为左侧原本有序,比 key 大的元素整体右移,key 被插入到正确位置,所以插入后,左侧扩展后的子数组仍然有序。
7.3 终止
当外层循环结束时,已经处理完所有元素。根据循环不变式,当前元素左边的部分已经有序。此时这个部分就是整个数组,因此整个数组有序。
所以插入排序正确。
8. 如何写出一个好的循环不变式
一个好的循环不变式,通常要满足三点:

- 足够强:循环结束后能推出最终结论;
- 能被保持:每轮执行后不会被破坏;
- 容易验证初始化:循环开始前可以成立。
9. 代码实践:用断言检查不变式
python
def is_sorted_prefix(arr, end):
for i in range(1, end):
if arr[i - 1] > arr[i]:
return False
return True
def insertion_sort_with_invariant_check(nums):
arr = nums[:]
assert is_sorted_prefix(arr, 1)
for j in range(1, len(arr)):
assert is_sorted_prefix(arr, j)
key = arr[j]
i = j - 1
while i >= 0 and arr[i] > key:
arr[i + 1] = arr[i]
i -= 1
arr[i + 1] = key
assert is_sorted_prefix(arr, j + 1)
assert is_sorted_prefix(arr, len(arr))
return arr
nums = [5, 2, 4, 6, 1, 3]
print(insertion_sort_with_invariant_check(nums))
输出:
text
[1, 2, 3, 4, 5, 6]
这些 assert 不是排序必须的一部分,而是帮助我们把循环不变式显式检查出来。
10. 常见误区

误区一:把循环条件当成循环不变式
例如 i < n 是循环条件,不是证明正确性的关键性质。
误区二:不变式太弱
比如只说"循环一直在处理数组元素",无法推出数组最终有序。
误区三:不变式太强
如果一开始就说"整个数组已经有序",初始化通常不成立。
误区四:只描述过程,不证明保持
很多人会说"每次插入到正确位置",但没有说明为什么插入后左侧仍然有序。
11. 现代延伸
循环不变式不只用于排序算法,它在很多工程场景中都有影子。
| 场景 | 类似的不变式思想 |
|---|---|
| 数据库事务 | 事务执行前后保持数据一致性 |
| 分布式系统 | 状态机复制要求节点状态一致 |
| 缓存淘汰 | 缓存结构始终满足淘汰策略 |
| 堆结构 | 每次插入删除后仍保持堆性质 |
| 红黑树 | 插入删除后仍保持颜色和平衡性质 |
| 图算法 | 每轮维护已确定节点集合的性质 |
很多复杂系统的正确性,本质上就是维护某种"不变性质"。
12. 思考题
- 循环不变式和循环条件有什么区别?
- 为什么插入排序中"左侧前缀有序"是一个合适的不变式?
- 初始化、保持、终止三步分别解决什么问题?
- 如果一个不变式无法推出最终结果,它有什么问题?
- 尝试给"寻找最大值"算法写一个循环不变式。
13. 本篇小结
本篇重点讲了循环不变式。
核心结论是:
- 循环不变式是循环过程中始终保持成立的关键性质;
- 正确性证明通常分为初始化、保持、终止三步;
- 插入排序中的不变式是:当前元素左侧的子数组已经有序;
- 测试只能发现部分错误,循环不变式可以帮助证明整体正确性。
以后看到循环算法时,可以多问一句:
text
这个循环到底一直维护着什么性质?
这句话往往就是理解算法正确性的关键。