今天来学习归并算法的一个应用,求数组中的逆序对数。
首先我们需要知道逆序对是什么东西:在一个数组中,有两个元素,索引小的元素比索引大的元素大,那这两个元素就构成一个逆序对。而一个数组中所有逆序对的个数就叫做逆序对数。
暴力求解法
我们可以很容易地想出暴力求解的方法:遍历数组,依次取数组中的每一个数,然后与索引排在其后的元素比较,如果比它小,则逆序对数+1,遍历完毕,得到的逆序对数则是数组的逆序对数。
代码如下:
/**
* 暴力求解法求逆序对数
* @param nums 数组
* @return 逆序对数
*/
public static int inversionPairCountBruteForce(int[] nums) {
Objects.requireNonNull(nums);
if (nums.length <= 1) {
// 数组最多只有一个元素,无法构成逆序对
return 0;
}
int inversionPairCount = 0;
// 遍历数组
for (int i = 0; i < nums.length - 1; i++) {
// 遍历该元素后续元素,查找逆序对
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] > nums[j]) {
// 找到则逆序对数+1
System.out.println("[" + nums[i] + ", " + nums[j] + "]");
inversionPairCount++;
}
}
}
return inversionPairCount;
}
暴力求解法的算法复杂度为O(n2),那么我们有没有更优的解法了呢?
在学点算法(三)——数组归并排序里面我们提到了分而治之的思想,在求逆序对的问题上,是否也可以用此思想呢?
归并算法求数组逆序对数
答案是可以的。我们一步步来分析:
- 要求一个数组的所有逆序对数,可以先将它分为两部分,左子数组和右子数组。
- 分割完毕后,一个数组的总逆序对数 = 左子数组的总逆序对数 + 右子数组的总逆序对数 + 左子数组和右子数组中交叉的逆序对数。
- 要求左(右)子数组的逆序对数我们可以使用递归的方法继续求,而左子数组和右子数组中交叉的逆序对数这个该怎么求呢?如果不施加任何条件,我们还是需要遍历左子数组中的元素,然后依次取右子数组中的元素进行对比,这样比较下来我们还是需要O(n2)的算法复杂度。
- 而我们知道如果经过归并排序后,左子数组和右子数组会分别变为有序的,那么我们在对比一组元素之后,如果发现逆序(左子数组中的元素比右子数组的元素大),那么右子数组的该元素会与左子树中后续所有元素构成逆序对。通过这个发现,我们可以利用归并排序算法代码,来求逆序对数,而该算法复杂度也优化到了和归并排序一致,为O(nlogn)。
我们取[53, 89]
和[32, 45, 67]
的归并来说明这个过程:
- 左子数组的
53
和右子数组的32
对比,发现53
大于32
,那么32
将和左子数组中53
及其以后的元素构成逆序对,即[53, 32]
,[89, 32]
。 - 左子数组的
53
和右子数组的45
对比,发现53
大于45
,那么45
将和左子数组中53
及其以后的元素构成逆序对,即[53, 45]
,[89, 45]
。 - 左子数组的
53
和右子数组的67
对比,发现53
小于45
,则无逆序对。
- 左子数组的
89
和右子数组的67
对比,发现89
大于67
,那么67
将和左子数组中89
及其以后的元素构成逆序对,即[89, 67]
。
- 右子数组中已无元素,则后续无逆序对。
累加每一次对比的结果:2 + 2 + 1
可以得到交叉的逆序对数为5
。
我们只需要在归并排序算法(归并排序算法请见学点算法(三)——数组归并排序)的基础上稍作修改即可得到求逆序数的算法:
/**
* 数组的归并排序算法(同时求逆序对数)
*
* @param nums 数组
* @param lo 区间的lo索引(包含)
* @param hi 区间的hi索引(不包含)
*/
public static int inversionPairCountMergeSort(int[] nums, int lo, int hi) {
// 数组为null则直接返回
if (nums == null) {
return 0;
}
// 索引检查
if (lo < 0 || nums.length <= lo) {
throw new IllegalArgumentException("lo索引必须大于0并且小于数组长度,数组长度:" + nums.length);
}
if (hi < 0 || nums.length < hi) {
throw new IllegalArgumentException("hi索引必须大于0并且小于等于数组长度,数组长度:" + nums.length);
}
if (hi <= lo) {
// lo索引必须小于hi索引(等于也不行,因为区间是左闭右开,如果等于,区间内元素数量就为0了)
throw new IllegalArgumentException("lo索引必须小于hi索引");
}
if (lo + 1 >= hi) {
// 区间元素个数最多为1
// 无需排序,逆序数为0
return 0;
}
int inversionPairCount = 0;
int mid = (lo + hi) / 2;
// 对左子区间排序,并加上逆序对数
inversionPairCount += inversionPairCountMergeSort(nums, lo, mid);
// 对右子区间排序,并加上逆序对数
inversionPairCount += inversionPairCountMergeSort(nums, mid, hi);
// 对两个排序好的子区间归并,得到一个整体有序的区间,并加上两个子区间交叉的逆序对数
inversionPairCount += merge(nums, lo, mid, hi);
return inversionPairCount;
}
public static int merge(int[] nums, int lo, int mid, int hi) {
// 这里不用检查索引,调用方已经决定了索引是有效的
// 结果区间和右子区间使用原有数组
// 左子区间使用临时数组(因为结果区间可能会覆盖左子区间的元素,所以需要开辟新数组保存)
int inversionPairCount = 0;
int leftLen = mid - lo;
int[] left = new int[leftLen];
System.arraycopy(nums, lo, left, 0, leftLen);
// 左子区间索引
int leftIdx = 0;
// 右子区间索引
int rightIdx = mid;
// 结果区间索引
int resultIdx = lo;
while (true) {
if (leftIdx < leftLen && rightIdx < hi) {
// 两个子区间都存在元素
// 取两个子区间的有效首元素对比
if (left[leftIdx] <= nums[rightIdx]) {
// 左子区间首元素小于右子区间首元素
// 将左子区间首元素放到结果位置,同时更新索引位置
nums[resultIdx++] = left[leftIdx++];
} else {
// 右子区间首元素小于左子区间首元素
// 将右子区间首元素放到结果位置,同时更新索引位置
nums[resultIdx++] = nums[rightIdx++];
for (int i = leftIdx; i < leftLen; i++) {
System.out.println("[" + left[i] + ", " + nums[rightIdx-1] + "]");
}
inversionPairCount += (leftLen - leftIdx);
}
} else {
if (leftIdx < leftLen) {
// 左子区间还有剩余元素
// 直接将左区间所有元素一起移动到结果位置
System.arraycopy(left, leftIdx, nums, resultIdx, leftLen - leftIdx);
} else {
// 右子区间还有剩余元素
// 因为经过上一次判断,左子区间和右子区间只会有一个存在剩余元素
// 直接将右区间所有元素一起移动到结果位置
System.arraycopy(nums, rightIdx, nums, resultIdx, hi - rightIdx);
}
// 全部元素移动完毕,退出
break;
}
}
return inversionPairCount;
}
测试代码如下:
int[] nums = {2, 343, 4, 1, 3, 5, 7};
System.out.println(inversionPairCountBruteForce(nums));
System.out.println(inversionPairCountMergeSort(nums, 0, nums.length));
输出如下:
[2, 1]
[343, 4]
[343, 1]
[343, 3]
[343, 5]
[343, 7]
[4, 1]
[4, 3]
暴力求解法求逆序对数:8
[343, 4]
[2, 1]
[4, 1]
[343, 1]
[4, 3]
[343, 3]
[343, 5]
[343, 7]
归并排序法求逆序对数:8
符合我们的预期。