排序算法之:比较计数排序(Comparison Counting Sort)的稳定版本实现与进阶优化(再次深度解析)
题目描述
给定一个包含 n 个元素的数组,元素可以是任意可比较类型(例如整数、字符串、自定义对象)。要求实现比较计数排序,这是一种基于比较的非比较排序算法。算法的核心思想是:对于数组中的每个元素,统计严格小于它的元素个数,这个计数值就直接决定了该元素在有序序列中的最终位置。然而,当存在相等元素时,这个简单映射会导致不稳定。本题要求:
- 实现稳定的比较计数排序,确保相等元素的相对顺序在排序后保持不变。
- 在稳定版本的基础上,进行进阶优化,降低空间复杂度并提高缓存友好性。
- 提供完整的正确性证明(基于循环不变式)和复杂度分析。
注意:常规的计数排序(Counting Sort)假设输入是有限范围内的整数,通过频率统计和前缀和直接放置元素,是稳定的,且并非基于元素间的比较。而这里的“比较计数排序”是通过比较操作统计“小于”关系,是一种基于比较的排序方法,但其时间复杂度可达 O(n²),通常用于教学或特定场景。本题的重点在于其稳定版本的实现细节与优化技巧。
解题过程循序渐进讲解
第一步:基础算法思想回顾
比较计数排序的基本步骤如下:
- 初始化一个大小为 n 的
count数组,count[i]用于记录在原始数组中严格小于arr[i]的元素个数。 - 对于每个位置 i(0 ≤ i < n),遍历所有位置 j(0 ≤ j < n),如果
arr[j] < arr[i],则count[i]++。 - 根据
count数组构造有序输出:创建一个大小为 n 的输出数组output,对于每个 i,将arr[i]放到output[count[i]]位置上。
问题:当存在相等元素时,若只统计“严格小于”,所有相等元素的 count 值都相同,会导致它们在输出数组中被放置到同一个目标位置,产生冲突,且顺序无法保证(不稳定)。例如,数组 [3a, 1, 3b](用下标区分相同值),count 值可能为 [1, 0, 1],两个 3 都试图放入 output[1],造成覆盖。
第二步:实现稳定版本——统计“小于或等于”并调整
为了保证稳定性,我们需要确保相等元素在输出数组中的相对顺序与输入一致。常用方法是:
- 修改统计规则:不仅统计“严格小于”,还额外统计“在原始顺序中出现在当前元素之前且值相等的元素”。
- 具体实现:
- 遍历每一对 (i, j),比较
arr[i]和arr[j]。 - 如果
arr[j] < arr[i],则count[i]++。 - 如果
arr[j] == arr[i]且j < i,则count[i]++。 - 这样,
count[i]表示的是“严格小于 arr[i] 的元素个数”加上“在 arr[i] 之前且与 arr[i] 相等的元素个数”,即“在最终有序序列中应排在 arr[i] 之前的元素总数”。
- 遍历每一对 (i, j),比较
- 放置元素:创建输出数组
output,遍历原始数组,对于每个arr[i],将其放入output[count[i]],然后count[i]++(可选,用于处理连续放置,但这里不必须,因为每个元素已有唯一目标位置)。
例子:数组 [3a, 1, 3b]
- 对于 i=0(值3a):比较 j=0,1,2 → 严格小于:j=1(1<3),相等且j<i:无 →
count[0]=1。 - 对于 i=1(值1):严格小于:无,相等且j<i:无 →
count[1]=0。 - 对于 i=2(值3b):严格小于:j=1(1<3),相等且j<i:j=0(值相等且j<i) →
count[2]=2。
得到count = [1, 0, 2],输出:output[0]=arr[1]=1,output[1]=arr[0]=3a,output[2]=arr[2]=3b,结果[1, 3a, 3b]稳定。
第三步:进阶优化(降低空间与提高效率)
基础方法需要 O(n²) 次比较和 O(n) 额外空间(count 和 output 各 n)。我们可以做以下优化:
优化1:减少比较次数
- 注意到在统计“小于或等于”时,每次比较涉及两个元素,但我们可以只对每个对 (i, j) 比较一次,而不是分别从 i 和 j 视角各比较一次。
- 实现:遍历所有无序对 (i, j) 且 i ≠ j。比较
arr[i]和arr[j]:- 如果
arr[i] > arr[j],则count[i]++。 - 如果
arr[i] == arr[j]且i < j,则count[j]++(这保证了相等时,下标小的元素不会“被提前”)。
- 如果
- 这样,每个对只比较一次,总比较次数为 C(n,2) = n(n-1)/2,仍为 O(n²),但常数减半。
优化2:原地重排(降低空间复杂度)
- 可以不使用额外的
output数组,而是在原数组上通过交换实现排序。 - 方法:根据
count数组,我们可以知道每个元素的目标索引。然后从 i=0 开始,如果count[i]不等于 i,则不断将arr[i]与arr[count[i]]交换,直到count[i] == i或遇到正确位置。但注意交换时需要同步交换count值以保持正确性。 - 该过程类似圈排序(Cycle Sort),每个元素最多被移动一次到最终位置,总交换次数最少,但实现需小心处理循环和相等元素的稳定性。
优化3:缓存友好性
- 在比较阶段,顺序访问数组元素,缓存命中率较高。
- 在放置阶段,如果使用额外输出数组,也是顺序写入,缓存友好。
- 如果原地重排,交换操作可能导致随机访问,降低缓存效率,但交换次数最少,整体可能仍有益。
第四步:算法步骤总结(稳定优化版)
- 初始化
count数组全为0。 - 对每一对 (i, j) 满足 0 ≤ i < j < n:
- 如果
arr[i] > arr[j],则count[i]++。 - 如果
arr[i] < arr[j],则count[j]++。 - 如果
arr[i] == arr[j],则count[j]++(这保证了当值相等时,下标较小的 i 对应的元素在最终序列中仍会在前面,因为下标较小的元素不会因为这次相等而被计入“小于”计数)。
- 如果
- 此时
count[k]表示应排在arr[k]前面的元素个数,即其目标索引。 - 创建输出数组
output,大小为 n。 - 对于每个 k,将
arr[k]放入output[count[k]]。 - 返回
output。
循环不变式证明稳定性:
- 不变式:在处理任何一对 (i, j) 后,对于所有下标对 (p, q) 满足 p < q 且值相等,
count[p]最终将小于count[q]。 - 初始:
count全0,满足。 - 保持:考虑任意值相等的对 (p, q) 且 p < q。当比较 (p, q) 时,由于值相等且 p < q,根据规则,
count[q]++,因此count[p]保持小于count[q]。其他比较不会改变这个顺序。 - 终止:最终所有对处理完毕,相等元素的下标顺序在
count中得以保持,从而在输出数组中保持稳定。
时间复杂度:
- 比较次数:恰好 n(n-1)/2 次,O(n²)。
- 空间复杂度:O(n)(
count和output各 n),原地版本可降至 O(1) 额外空间(但需修改原数组)。
第五步:进一步讨论
- 该算法虽然简单,但在实际中因 O(n²) 比较很少用于大规模数据,常用于教学或作为子程序在小数据上使用。
- 优化版本保证了稳定性,且通过成对比较一次减少了常数因子。
- 与常规计数排序不同,它不依赖值范围,适用于任何可比较类型,但代价是更高的时间复杂度。
通过以上步骤,我们完成了稳定版本的比较计数排序的实现与优化,并给出了正确性证明和复杂度分析。