在解决涉及数组排序和逆序对计算的算法题时,归并排序方法是一个极其有效的工具。本文将通过解析一个具体的算法问题来全面理解归并排序及其在计算数组中逆序对数量时的应用。
题目链接:数组中的逆序对_牛客题霸_牛客网 (nowcoder.com)
题解来自官方,本文只是个人对题解的感悟
给定一个数组,我们需要计算这个数组中逆序对的总数。逆序对定义为数组中一对数字,其中前一个数字大于后一个数字。例如,在数组 [4, 3, 2, 1]
中,逆序对包括 (4, 3)
, (4, 2)
, (4, 1)
, (3, 2)
, (3, 1)
, (2, 1)
。
归并排序:一种高效的排序算法,采用分治策略,将问题分成较小的子问题解决后再合并。
逆序对:在数组中,前面的数字大于后面的数字形成的对。
public int mergeSort(int left, int right, int[] data, int[] temp) {
// 停止划分的条件:当子数组只有一个元素时
if (left >= right)
return 0;
// 计算中间位置,用于分割数组
int mid = (left + right) / 2;
// 递归地对左半部分和右半部分进行归并排序,并计算逆序对
int res = mergeSort(left, mid, data, temp) +
mergeSort(mid + 1, right, data, temp);
// 防止结果溢出
res %= mod;
// 合并两个已排序的子数组的过程
int i = left, j = mid + 1;
for (int k = left; k <= right; k++)
temp[k] = data[k];
for (int k = left; k <= right; k++) {
if (i == mid + 1) {
// 左侧子数组已完全处理完,直接取右侧元素
data[k] = temp[j++];
} else if (j == right + 1 || temp[i] <= temp[j]) {
// 右侧子数组已处理完或者左侧元素较小,取左侧元素
data[k] = temp[i++];
} else {
// 发现逆序对:左侧元素大于右侧元素
data[k] = temp[j++];
// 增加逆序对的数量:右侧当前元素与左侧剩余元素形成逆序对
res += mid - i + 1;
}
}
return res % mod;
}
归并排序是一种典型的分治算法。
划分:将数组从中间分开,形成两个子数组。
递归排序:分别对这两个子数组进行归并排序。
合并:将两个有序的子数组合并成一个有序数组。
归并排序通过递归地将数组分成更小的部分,然后分别对这些部分进行排序,最后将它们合并成一个完整的有序数组。
分割数组:
数组被分割成两部分。left
和 right
是数组当前处理部分的起始和结束索引。
mid
是中间索引,计算方法是 (left + right) / 2
。
数组被分割成 [left, mid]
和 [mid + 1, right]
两个部分。
递归调用:
mergeSort(left, mid, data, temp)
递归地处理数组的左半部分。
mergeSort(mid + 1, right, data, temp)
递归地处理数组的右半部分。
递归的含义:
这两个递归调用继续将各自的子数组分割成更小的部分,直到每个子数组只包含一个元素(此时自然是有序的)。
在递归返回后,两个子数组已经被分别排序。
这时,mergeSort
函数的后续部分会将这两个有序的子数组合并成一个有序数组。
在合并过程中,还会计算和累加逆序对的数量。
通过这种方法,归并排序不仅能有效地对数组进行排序,还能在排序过程中计算出数组中的逆序对数量。
归并排序的合并阶段为我们提供了计算逆序对的绝佳机会。当我们合并两个子数组时,如果左侧数组的元素大于右侧数组的元素,那么左侧数组当前元素及其后面的所有元素都将与右侧的这个元素形成逆序对。
这部分代码是 mergeSort
函数中的核心,它负责合并两个已排序的子数组,并在这个过程中计算逆序对的数量。我将逐步解释这一过程。
初始化索引:
int i = left, j = mid + 1;
:这里初始化了两个索引,i
用于遍历左侧子数组(从 left
到 mid
),j
用于遍历右侧子数组(从 mid + 1
到 right
)。
复制数组到临时数组:
for (int k = left; k <= right; k++) { temp[k] = data[k]; }
:这个循环将当前处理的数组部分复制到 temp
数组中。这样做是为了保留原数组的数据,因为接下来的合并操作会直接修改 data
数组。
合并操作:
在接下来的循环中,for (int k = left; k <= right; k++)
,循环变量 k
用于追踪当前正在处理的元素的位置。
处理条件:
if (i == mid + 1)
:如果左侧子数组已经完全处理完(即 i
已经超过了中间索引 mid
),则直接从右侧子数组取元素放入 data
。
else if (j == right + 1 || temp[i] <= temp[j])
:如果右侧子数组已经处理完(即 j
超过了 right
)或左侧当前元素小于等于右侧当前元素,从左侧子数组取元素放入 data
。
else
:这是处理逆序对的关键部分。如果左侧元素大于右侧元素,那么左侧的这个元素和它右边的所有元素(因为左侧子数组是有序的)都会与右侧的这个元素形成逆序对。
计算逆序对:
res += mid - i + 1;
:当发现一个逆序对时,实际上找到了多个逆序对。所有在左侧子数组中,从当前 i
位置到 mid
的元素都会与右侧的 j
位置的元素形成逆序对。因此,逆序对的数量增加了 mid - i + 1
(这是左侧子数组中未处理元素的数量)。
通过这种方式,函数在合并两个子数组的同时,准确地统计了逆序对的数量。这种方法充分利用了归并排序过程中的“自然顺序”,从而有效地计算逆序对,而不需要进行额外的、成本高昂的比较操作。
data
数组data
是我们要排序的原始数组,同时它也是函数最终将排序结果存放的地方。
在归并排序过程中,data
数组会被不断地分成更小的部分,直到每个部分只包含一个元素。
排序的最终目标是修改 data
数组,使其成为一个有序数组。
temp
数组temp
是一个辅助数组,其大小与 data
相同,用于临时存储 data
数组的元素。
在合并两个已排序的子数组时,我们不能直接在 data
数组上进行操作,因为这样会覆盖还未处理的元素。所以,我们首先将 data
数组的内容复制到 temp
数组中。
然后,我们从 temp
数组中取出元素来进行比较和合并,最终将合并后的有序序列写回 data
数组。
为什么使用 temp
数组?
直接在 data
数组上操作会使得排序过程复杂,因为合并操作可能会覆盖还未处理的元素。
使用 temp
数组可以避免这个问题,因为它允许我们在合并时“查看”原始元素,同时将排序好的元素“写回”到 data
数组中。
这种方法简化了逻辑,并确保了归并排序的正确性和高效性。
为什么逆序对的数量是 mid - i + 1
?
因为左侧数组是有序的,所以如果一个左侧元素大于右侧的元素,那么左侧这个元素及其后面的所有元素都大于这个右侧的元素。因此,我们一次性计算出多个逆序对。
mergeSort
函数中的使用复制到 temp
:
在合并之前,data
数组中的元素被复制到 temp
数组中。for (int k = left; k <= right; k++) temp[k] = data[k];
这一行完成了复制操作。
temp
数组上进行的。我们比较 temp
数组中左右两部分的元素,并按顺序选择较小的元素放入 data
数组中。 如果左侧元素小于或等于右侧元素,我们将左侧元素写入 data
数组。如果左侧元素大于右侧元素,我们将右侧元素写入 data
数组,并计算逆序对。data
:data
数组将包含从 left
到 right
索引范围内排序好的元素。