排序算法之:循环不变式在循环排序(Cyclic Sort)中的正确性证明
题目描述
循环排序(Cyclic Sort)是一种原地、不基于比较的排序算法,特别适用于对元素值在特定范围内的数组进行排序,例如对包含数字 1 到 n 的数组进行排序。其核心思想是将每个元素直接放置到其在最终排序数组中的正确位置上。本题要求详细阐述循环排序算法的工作原理,并利用循环不变式(Loop Invariant)来严格证明该算法的正确性。
解题过程
-
算法思想与步骤
循环排序适用于对元素为离散整数且范围已知的数组进行排序。一个典型的应用场景是:给定一个长度为 n 的数组,其中的元素是来自 1 到 n 的整数,且每个元素只出现一次。算法的目标是将这些元素按升序排列。-
基本思想:对于数组中的每个位置
i(从 0 到 n-1),该位置在排序后应该存放值为i+1的元素。算法尝试将当前位于位置i的元素x交换到其“正确”的位置,即索引为x - 1的位置上(因为如果元素 x 在排序后应该在索引 x-1 处)。这个“放置”过程可能会循环进行,直到当前位置i被放置了正确的元素为止。 -
算法步骤:
- 初始化一个指针
i,从数组的起始索引 0 开始遍历。 - 当
i小于数组长度n时,执行循环:
a. 设当前索引i处的元素为x。
b. 计算该元素x的“正确”位置(目标位置)correct_index = x - 1。
c. 如果x已经在它的正确位置上(即i == correct_index),则简单地递增i,处理下一个位置。
d. 否则,将当前元素x与位于其正确位置correct_index上的元素进行交换。
e. 关键点:交换后,原先在位置i的元素x被放置到了正确的位置correct_index。但是,此时被交换到位置i的新元素可能还不是位置i的正确元素。因此,指针i在此轮不递增,而是继续检查和处理当前这个新交换过来的元素,直到位置i被放置了正确的元素(即值为i+1的元素)为止。
- 初始化一个指针
-
-
循环不变式的定义
为了证明循环排序的正确性,我们需要定义一个合适的循环不变式。循环不变式是算法循环中一个在每次循环迭代开始和结束时都为真的条件(谓词)。它帮助我们推理算法的状态变化,并最终证明算法的正确性。-
循环不变式定义:
对于循环排序算法的主循环(步骤2),在每次循环迭代开始时,以下条件成立:
“对于所有索引j,其中0 <= j < i,位置j上的元素已经是其最终的正确元素(即arr[j] == j + 1)。” -
解释:
这个不变式断言:在指针i之前的所有位置(即索引小于i的位置),都已经被正确地排序了。这些位置的元素已经是它们最终应该有的值。
-
-
循环不变式的证明
证明循环不变式需要三个部分:初始化、保持和终止。-
初始化(Initialization):
- 时机:在循环的第一次迭代开始之前。
- 状态:此时指针
i = 0。不变式中描述的范围是j从 0 到i-1,即j < 0。这是一个空集,不存在任何索引j满足0 <= j < 0。 - 结论:对于一个空的范围,断言“所有位置
j上的元素已经是正确元素”是平凡真(vacuously true)的。因此,循环不变式在初始化时成立。
-
保持(Maintenance):
-
目标:假设在循环的某次迭代开始时,循环不变式成立(即索引
0到i-1的位置都已正确排序),我们需要证明在这次迭代执行完毕后,循环不变式仍然成立(即索引0到i的位置——如果i增加了——或仍然是0到i-1——如果i没变——都已正确排序)。 -
情况分析:根据算法步骤2:
- 情况 A:如果
arr[i]已经是正确元素(arr[i] == i + 1)。算法会递增i。在递增之后,新的i是原来的i+1。在本次迭代开始时,我们知道索引0到i-1(旧i)是正确的。而由于我们检查发现位置i(旧i)本身也是正确的,所以在递增i之后,索引0到i-1(新i,即旧i+1减1)仍然是正确的。循环不变式得以保持。 - 情况 B:如果
arr[i]不是正确元素。算法会将其与它正确位置(correct_index)上的元素交换。- 关键观察 1:
correct_index的值是多少?由于数组元素是 1 到 n 的排列,correct_index = arr[i] - 1。这个值不可能小于i吗?有可能等于或大于i。但重要的是,它不可能小于i。为什么?因为如果correct_index < i,那么根据循环不变式(在本次迭代开始时成立),位置correct_index上应该已经是正确的元素(值为correct_index + 1)。但是,我们当前在位置i的元素是x,其正确位置正是correct_index,这意味着值x应该等于correct_index + 1。如果correct_index < i,那么位置correct_index上已经有的值就是correct_index + 1,也就是x。这与我们当前在位置i也发现元素x相矛盾,因为每个元素只出现一次。因此,correct_index必须大于或等于i。 - 关键观察 2:由于
correct_index >= i,并且correct_index != i(否则就是情况 A 了),所以correct_index > i。这意味着我们交换的元素位于尚未处理的范围(i或之后)。 - 交换操作的影响:交换操作将正确的元素
x放到了位置correct_index。同时,将一个(可能不正确的)新元素从位置correct_index带到了位置i。 - 指针
i不变:算法此时不递增i。 - 不变式保持:在这次迭代结束时,指针
i没有改变。索引小于i的所有位置,在本次迭代中没有被修改(因为交换发生在位置i和correct_index,而correct_index >= i,所以索引< i的位置 untouched)。因此,在迭代结束时,“索引0到i-1已正确排序”这一条件仍然成立。循环不变式得以保持。
- 关键观察 1:
- 情况 A:如果
-
总结:无论在情况 A 还是情况 B,每次迭代结束后,循环不变式都保持为真。
-
-
终止(Termination):
- 时机:当循环条件
i < n不再满足时,循环终止。此时i == n。 - 应用不变式:根据循环不变式,在循环终止时,对于所有索引
j,满足0 <= j < n(因为i = n),位置j上的元素已经是其正确元素(arr[j] == j + 1)。 - 结论:这意味着整个数组(索引 0 到 n-1)都已经按升序排好序。因此,算法是正确的。
- 时机:当循环条件
-
-
总结
通过定义并证明“索引小于i的位置已正确排序”这一循环不变式,我们严谨地论证了循环排序算法能够逐步地、正确地将元素放置到其最终位置上。循环不变式在初始化时为真,在每次迭代中得以保持,并在循环终止时引导出整个数组已排序的正确结论,从而完整地证明了算法的正确性。