排序算法之:Gnome Sort 中的“交换退步”过程与排序终止条件的严格证明
我将详细讲解地精排序(Gnome Sort)的详细过程,并严格证明其排序终止的正确性。地精排序因其简单性而闻名,其核心过程就像园丁整理花盆:一次前进一步,发现顺序不对就向后交换。
问题描述
给定一个长度为 n 的整数数组 arr,要求使用 Gnome Sort(地精排序) 将其原地排序为升序序列。你需要理解并证明其排序过程为何最终能正确终止,并得到一个完全有序的数组。
输入: arr = [5, 3, 2, 4, 1]
输出: [1, 2, 3, 4, 5]
核心要求:
- 掌握 Gnome Sort 的基本步骤。
- 证明其“交换退步”过程不会陷入无限循环。
- 证明当过程终止时,数组是完全有序的。
解题过程详解
我们将分三步进行:算法描述与演示 -> 终止性证明 -> 正确性证明。
步骤 1:算法描述与过程演示
Gnome Sort 的算法逻辑异常简单,只需要一个指针 pos 表示“园丁”当前所在的位置:
- 初始化:令
pos = 1(表示从第二个元素开始向前比较)。 - 主循环:当
pos < n时,执行:- 情况 A(前进):如果
pos == 0或者arr[pos] >= arr[pos - 1],说明当前位置或之前的顺序是对的,园丁向前迈一步(pos += 1)。 - 情况 B(交换并后退):否则(即
pos > 0且arr[pos] < arr[pos - 1]),说明当前位置的元素应该排到前一个元素的前面。于是交换arr[pos]和arr[pos - 1],然后园丁向后退一步(pos -= 1)去检查这个被交换过来的元素是否还需要继续向前移动。
- 情况 A(前进):如果
- 终止:当
pos == n时,循环结束,数组已排序。
手动模拟 arr = [5, 3, 2, 4, 1]:
- 初始:
[5, 3, 2, 4, 1],pos=1。 pos=1: 3 < 5? 是。交换[3, 5, 2, 4, 1],pos退到 0。pos=0: 等于0,前进到1。pos=1: 5 < 3? 否。前进到2。pos=2: 2 < 5? 是。交换[3, 2, 5, 4, 1],pos退到1。pos=1: 2 < 3? 是。交换[2, 3, 5, 4, 1],pos退到0。pos=0: 前进到1。pos=1: 3 < 2? 否。前进到2。pos=2: 5 < 3? 否。前进到3。pos=3: 4 < 5? 是。交换[2, 3, 4, 5, 1],pos退到2。pos=2: 4 < 3? 否。前进到3。pos=3: 5 < 4? 否。前进到4。pos=4: 1 < 5? 是。交换[2, 3, 4, 1, 5],pos退到3。pos=3: 1 < 4? 是。交换[2, 3, 1, 4, 5],pos退到2。pos=2: 1 < 3? 是。交换[2, 1, 3, 4, 5],pos退到1。pos=1: 1 < 2? 是。交换[1, 2, 3, 4, 5],pos退到0。pos=0: 前进到1。pos=1: 前进到2。pos=2: 前进到3。pos=3: 前进到4。pos=4: 前进到5。pos=5等于n=5,循环终止。得到[1, 2, 3, 4, 5]。
从模拟可以看出,算法就像是通过连续的“交换-后退”操作,将较小的元素像气泡一样“冒”到前面合适的位置,而大的元素则自然地向后滑动。
步骤 2:终止性证明(证明算法一定会结束)
我们需要证明,对于任何长度为 n 的有限数组,上述过程总是在有限步内使 pos 达到 n 并终止。
我们引入一个关键的不变量,称为 “能量函数”或“势函数”。
定义:E = pos + 2 * (当前数组的逆序对数量)。
我们来分析 E 在算法每次迭代中的变化:
- 在“前进”步骤(情况 A):
pos增加了 1。逆序对数量可能减少、不变或增加吗?仔细分析:当pos前进时,意味着arr[pos] >= arr[pos-1]。这个操作本身并不改变数组,所以逆序对数量不变。因此,总能量E增加 1。 - 在“交换并后退”步骤(情况 B):此时
arr[pos] < arr[pos-1]。交换这两个元素。- 对于逆序对数量的影响:交换前,
(pos-1, pos)构成一个逆序对。交换后,这个逆序对被消除。但交换可能会影响其他位置上的逆序对。可以证明,交换相邻的逆序对,总逆序对数量严格减少 1。因为这对交换只改变了这两个元素与它们之间、以及它们与其他元素的关系,并且因为它们是相邻的,所以不会引入新的逆序对,只会消除这一个。因此,逆序对数量 减少 1。 pos减少了 1。- 因此,
E的变化量 =(pos-1) + 2*(逆序数-1) - [pos + 2*逆序数] = -1 - 2 = -3。即E严格减少 3。
- 对于逆序对数量的影响:交换前,
结论:
- 在每次循环迭代中,能量
E要么增加1,要么减少3。 - 初始时,
0 <= pos < n,逆序数最大为n(n-1)/2,所以E有一个有限的上界。 - 更重要的是,
E有一个明确的下界:pos最小为0,逆序数最小为0,所以E >= 0。 - 由于每次迭代
E要么增加1,要么减少3,且E有下界,因此它不能无限次地减少。同时,因为E每次增加1后,只要还有逆序对在pos附近,就可能触发减少。但关键在于,E不能无限变化(既不能无限增加,也不能无限振荡),因为它被限制在有限范围[0, n + n(n-1)]内。 - 更直接的推理:由于每次交换(减少
E)都严格减少逆序对总数,而逆序对总数是有限非负整数,所以交换步骤只能发生有限次。交换步骤有限,前进步骤在两次交换之间可能发生多次,但总共的前进步数也受限于n加上交换引起的后退步数,因此总迭代次数有限。 - 最终,有限次迭代后,
pos必然达到n而终止。
步骤 3:正确性证明(证明终止时数组有序)
我们使用 循环不变式(Loop Invariant) 来证明。
循环不变式:在每次循环条件 pos < n 被评估之前,子数组 arr[0...pos-1] 是有序的(升序)。
我们来证明这个不变式:
- 初始化:在第一次循环开始前,
pos = 1。子数组arr[0...0]只有一个元素,自然是有序的。不变式成立。 - 保持:
- 假设在某次迭代开始时,
arr[0...pos-1]有序。 - 执行一次循环体:
- 情况 A(前进):触发条件为
pos==0或arr[pos] >= arr[pos-1]。- 如果
pos==0,前进后新的pos是1,子数组arr[0...0]仍有序。 - 如果
arr[pos] >= arr[pos-1],由于arr[0...pos-1]有序,且arr[pos]不小于该有序部分的最后一个元素,因此将arr[pos]并入后,arr[0...pos]也是有序的。前进后,pos变为pos+1,新的子数组arr[0...(pos+1)-1]即arr[0...pos]就是刚才证明有序的那个,不变式保持。
- 如果
- 情况 B(交换并后退):触发条件为
pos > 0且arr[pos] < arr[pos-1]。我们交换这两个元素。- 交换后,
arr[pos-1](原arr[pos],较小的值)可能破坏了arr[0...pos-1]的有序性。所以pos后退一步,新的pos变成了pos-1。现在的子数组范围arr[0...(pos-1)-1]即arr[0...pos-2]在交换前是有序的,且交换只影响了arr[pos-1]和arr[pos],对于索引小于pos-1的元素没有影响,所以arr[0...pos-2]仍然有序。不变式对新的pos仍然成立(因为新的子数组是旧的子数组去掉最后一个元素)。
- 交换后,
- 情况 A(前进):触发条件为
- 可见,无论哪种情况,在下一次循环条件判断时,不变式
arr[0...pos-1]有序仍然保持。
- 假设在某次迭代开始时,
- 终止:当循环终止时,条件
pos < n为假,即pos == n。根据循环不变式,此时arr[0...n-1]是有序的。这正是整个数组。所以算法正确。
总结
地精排序是一个非常直观的“交换-后退”式排序算法:
- 过程:它通过不断比较相邻元素,并在顺序错误时交换,同时后退一步来保证交换过来的元素继续参与比较,直到它到达正确位置。
- 终止性:通过引入“能量函数”(
E = pos + 2 * 逆序数)或直接观察“交换操作严格减少逆序对”,可以证明算法在有限步内必然结束。 - 正确性:通过循环不变式(“
pos之前的子数组始终有序”)可以严格证明,当pos移动到数组末尾时,整个数组有序。
它的时间复杂度在最好情况下(已排序)是 O(n),最坏和平均情况下是 O(n²),空间复杂度为 O(1)。虽然效率不高,但其简洁的逻辑和易于证明的正确性,使其成为理解排序算法基础和循环不变式概念的绝佳示例。