逆序对的数量
逆序对数量
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i < j 且 a[i] > a[j],则其为一个逆序对;否则不是。
所用方法和基本原理
该代码使用归并排序的思想来计算逆序对的数量。基本原理如下:
- 分解:和归并排序一样,将给定的数组不断二分,直到每个子数组只包含一个元素。这是因为单个元素的子数组不存在逆序对。
- 解决:递归地对左右两个子数组进行排序,在这个过程中,左右子数组内部的逆序对会被分别统计。
- 合并:在合并两个已经有序的子数组时,统计跨越两个子数组的逆序对。当左子数组的当前元素
arr[i]
大于右子数组的当前元素arr[j]
时,说明左子数组中从i
到mid
的所有元素都与arr[j]
构成逆序对,所以逆序对数量增加mid - i + 1
。同时将较小的元素(arr[j]
)放入临时数组。如果arr[i]
小于等于arr[j]
,则直接将arr[i]
放入临时数组。
代码及注释
public class InversionPairs {// 用于记录逆序对的数量public static long nums = 0;// 归并排序函数,同时计算逆序对public static void mergeSort(int[] arr, int l, int r) {// 如果l等于r,说明只有一个元素,没有逆序对,直接返回if (l == r) return;// 计算中间位置midint mid = l + (r - l >> 1);// 递归对左半部分数组进行归并排序并统计逆序对mergeSort(arr, l, mid);// 递归对右半部分数组进行归并排序并统计逆序对mergeSort(arr, mid + 1, r);// 计算当前需要合并的数组长度int tmpLen = r - l + 1;// 创建一个临时数组tmpArr,用于存储合并过程中的数据int[] tmpArr = new int[tmpLen];// 左半部分数组的起始索引int i = l;// 右半部分数组的起始索引int j = mid + 1;// 临时数组的索引int tmpIdx = 0;// 比较左右两个子数组的元素,将较小的元素放入临时数组,并统计逆序对while (i <= mid && j <= r) {if (arr[i] > arr[j]) {tmpArr[tmpIdx++] = arr[j++];// 当arr[i] > arr[j]时,左子数组中从i到mid的所有元素都与arr[j]构成逆序对nums += mid - i + 1;} else {tmpArr[tmpIdx++] = arr[i++];}}// 将左半部分数组剩余的元素放入临时数组while (i <= mid) {tmpArr[tmpIdx++] = arr[i++];}// 将右半部分数组剩余的元素放入临时数组while (j <= r) {tmpArr[tmpIdx++] = arr[j++];}// 将临时数组中的元素复制回原数组对应的位置for (int k = 0; k < tmpLen; k++) {arr[l + k] = tmpArr[k];}}
}
举例说明
假设有数组 arr = [3, 1, 2, 4]
。
- 分解阶段:
- 初始数组长度为 4,
l = 0
,r = 3
,计算mid = 0 + (3 - 0 >> 1) = 1
。 - 递归对左半部分
[3, 1]
进行排序,再次二分,mid = 0 + (1 - 0 >> 1) = 0
,得到两个子数组[3]
和[1]
。 - 对右半部分
[2, 4]
进行类似二分,得到[2]
和[4]
。
- 初始数组长度为 4,
- 合并阶段:
- 合并
[3]
和[1]
,arr[i] = 3
,arr[j] = 1
,因为3 > 1
,所以nums += 1 - 0 + 1 = 2
,将1
放入临时数组,再将3
放入,得到[1, 3]
。 - 合并
[2]
和[4]
,arr[i] = 2
,arr[j] = 4
,因为2 < 4
,直接将2
放入临时数组,再将4
放入,得到[2, 4]
。 - 最后合并
[1, 3]
和[2, 4]
,i = 0
(指向1
),j = 0
(指向2
),1 < 2
,将1
放入临时数组,i
后移。3 > 2
,nums += 1 - 1 + 1 = 1
,将2
放入临时数组,j
后移。3 < 4
,将3
放入临时数组,i
后移。最后将4
放入临时数组,得到[1, 2, 3, 4]
。 - 最终
nums = 2 + 1 = 3
,即逆序对数量为 3。
- 合并
方法的优劣
- 时间复杂度:
- 时间复杂度在最好、最坏和平均情况下均为 (O(n \log n))。这是因为和归并排序类似,每次分解都将数组分成两部分,共需要 (\log n) 层递归,而每层递归中合并操作(包括逆序对统计)的时间复杂度为 (O(n)),所以总的时间复杂度为 (O(n \log n))。
- 空间复杂度:
- 空间复杂度为 (O(n)),主要是因为在合并过程中需要一个与原数组长度相同的临时数组来存储合并的结果。
优点:
- 能够在 (O(n \log n)) 的时间复杂度内高效地计算逆序对数量,相比暴力解法 (O(n^2)) 的时间复杂度有很大提升。
- 借助归并排序的框架,代码结构清晰,易于理解和实现。
缺点: - 空间复杂度较高,需要额外的 (O(n)) 空间用于临时数组。在内存紧张的情况下可能不太适用。