基于逆序对的部分有序数组合并优化
字数 3064 2025-12-09 12:23:42
基于逆序对的部分有序数组合并优化
题目描述:给定两个已排序的数组 A 和 B,但 A 和 B 的合并数组可能不是完全有序的。具体来说,A 和 B 的合并数组包含一些“逆序对”(即 i < j 但 a_i > a_j),但这些逆序对的数量相对较少(记为 k,k 远小于合并后数组的总长度 n)。你的任务是在 O(n + k log k) 时间内,将合并后的数组排序。你不能简单地使用标准的归并排序(O(n)),因为它没有利用“逆序数量 k 很小”的特性。你需要设计一个更高效的算法,在 k 相对较小时显著优于 O(n log n)。
解题过程:
-
问题理解与目标
- 我们有两个已排序的数组 A 和 B。将 A 和 B 简单拼接(或按原始顺序合并)后得到一个数组 C。C 中可能存在一些逆序对,但总逆序对数 k 远小于 n(n = |A| + |B|)。
- 目标:对 C 进行排序,时间复杂度为 O(n + k log k),而不是标准的 O(n log n)。
- 关键观察:由于 A 和 B 分别有序,所以逆序对只能出现在 A 的元素与 B 的元素之间,且一定是 A 的某个较大元素在 B 的某个较小元素之前(即“跨数组逆序”)。
-
算法思想
- 核心思路是先进行一次粗略的归并,识别出所有可能产生逆序的元素,然后只对这些元素进行精细排序。
- 具体步骤:
a. 用两个指针 i 和 j 分别遍历 A 和 B,进行标准归并,但记录那些导致逆序的元素对。
b. 将所有涉及逆序的元素收集到一个“待修复列表”中。
c. 对这个“待修复列表”单独排序。
d. 将排序后的列表插回原数组的正确位置。
-
逐步详解
步骤1:识别逆序对元素
- 初始化指针 i=0(指向 A),j=0(指向 B),以及一个临时列表
to_fix用于存放涉及逆序的元素。 - 同时,我们维护一个“已排序部分”的列表
sorted_part,用于存放那些确定不会产生逆序的元素。 - 标准归并逻辑:比较 A[i] 和 B[j],将较小的元素放入
sorted_part,并移动相应指针。 - 关键修改:当 A[i] <= B[j] 时,说明 A[i] 不会与当前或之后的 B 元素产生逆序,所以 A[i] 可以直接放入
sorted_part。 - 但当 A[i] > B[j] 时,说明产生了逆序。此时,我们不能直接将 B[j] 放入
sorted_part,因为 B[j] 可能比 A 中后面的元素小,从而产生更多逆序。 - 因此,当 A[i] > B[j] 时,我们将 B[j] 加入
to_fix列表,并移动 j 指针。但注意,A[i] 仍然保留,继续与下一个 B 元素比较。
步骤2:收集所有待修复元素
- 继续上述过程,直到其中一个数组遍历完。
- 如果 A 有剩余,则剩余所有 A 元素都不会再产生逆序(因为 B 已空),所以可以直接全部加入
sorted_part。 - 如果 B 有剩余,则剩余的所有 B 元素都可能是逆序的一部分,因为它们都小于之前已放入
sorted_part的某些 A 元素(具体来说是那些导致逆序的 A 元素)。所以剩余的所有 B 元素都应加入to_fix列表。
步骤3:处理待修复列表
- 此时,
to_fix列表中包含了所有涉及逆序的元素。注意,这个列表的长度就是逆序对的数量 k(实际元素个数可能小于 k,但可以证明其长度是 O(k))。 - 我们对
to_fix列表进行排序,时间复杂度为 O(k log k)。
步骤4:合并最终结果
- 现在我们有
sorted_part(已有序,且与to_fix中的最小元素也满足有序性,因为sorted_part中的最大元素小于等于to_fix中的最小元素?不一定!需要验证)。 - 实际上,我们需要重新合并:将
sorted_part和已排序的to_fix列表进行一次标准归并,即可得到最终有序数组。这一步时间复杂度为 O(n)。
- 初始化指针 i=0(指向 A),j=0(指向 B),以及一个临时列表
-
正确性分析
- 在第一次归并中,所有放入
sorted_part的元素,都满足:对于任何已放入sorted_part的元素 x 和任何已放入to_fix的元素 y,有 x <= y。这是因为,每当 A[i] > B[j] 时,我们选择将 B[j] 放入to_fix而不是放入sorted_part,从而保证了sorted_part中的所有元素都小于等于当前 B[j] 以及之后的所有 B 元素(即所有to_fix中的元素)。 - 因此,
sorted_part自身有序,且整体小于等于to_fix中的所有元素。所以最后只需要将sorted_part和排序后的to_fix进行归并即可。
- 在第一次归并中,所有放入
-
时间复杂度分析
- 第一次遍历 A 和 B:O(n)。
- 排序
to_fix列表:O(k log k),其中 k 是to_fix列表的长度(即逆序对数量)。 - 最后归并:O(n)。
- 总时间复杂度:O(n + k log k)。当 k 很小时,这比 O(n log n) 好得多。
-
例子演示
设 A = [1, 4, 7], B = [2, 3, 6]。合并后数组 C = [1, 4, 7, 2, 3, 6]。- 逆序对有:(4,2), (4,3), (7,2), (7,3), (7,6),共 k=5。
- 算法过程:
- i=0, j=0: A[0]=1 <= B[0]=2 → 放1到 sorted_part,i=1。
- i=1, j=0: A[1]=4 > B[0]=2 → 放2到 to_fix,j=1。
- i=1, j=1: A[1]=4 > B[1]=3 → 放3到 to_fix,j=2。
- i=1, j=2: A[1]=4 <= B[2]=6 → 放4到 sorted_part,i=2。
- i=2, j=2: A[2]=7 > B[2]=6 → 放6到 to_fix,j=3 (B结束)。
- A 还有剩余,将剩余 A[2]=7 放入 sorted_part。
- 此时 sorted_part = [1,4,7],to_fix = [2,3,6]。
- 排序 to_fix 得到 [2,3,6]。
- 归并 sorted_part 和 to_fix:比较 1 和 2 → 1;比较 4 和 2 → 2;比较 4 和 3 → 3;比较 4 和 6 → 4;比较 7 和 6 → 6;最后 7 → 最终结果 [1,2,3,4,6,7]。
-
边界情况
- 如果 k=0(即 A 和 B 归并后已经有序),则 to_fix 为空,算法退化为标准归并,时间复杂度 O(n)。
- 如果 k 很大(比如接近 n^2),则 to_fix 列表可能包含几乎所有元素,此时排序 to_fix 的代价为 O(n log n),但总时间复杂度仍然是 O(n log n),不会比标准算法差太多。
-
扩展思考
- 这个算法可以推广到多个已排序数组的合并,只要逆序对总数 k 很小。
- 在实际应用中,这种“部分有序”情况常见于增量更新排序列表、多路归并的中间状态等场景。
这个算法巧妙地利用了两个子数组分别有序的条件,将问题规模从 n 缩小到 k,从而在逆序较少时获得显著效率提升。