学点算法(五)——使用归并算法求数组的逆序对数

今天来学习归并算法的一个应用,求数组中的逆序对数。

首先我们需要知道逆序对是什么东西:在一个数组中,有两个元素,索引小的元素比索引大的元素大,那这两个元素就构成一个逆序对。而一个数组中所有逆序对的个数就叫做逆序对数

暴力求解法

我们可以很容易地想出暴力求解的方法:遍历数组,依次取数组中的每一个数,然后与索引排在其后的元素比较,如果比它小,则逆序对数+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),那么我们有没有更优的解法了呢?

学点算法(三)——数组归并排序里面我们提到了分而治之的思想,在求逆序对的问题上,是否也可以用此思想呢?

归并算法求数组逆序对数

答案是可以的。我们一步步来分析:

  1. 要求一个数组的所有逆序对数,可以先将它分为两部分,左子数组和右子数组。
  2. 分割完毕后,一个数组的总逆序对数 = 左子数组的总逆序对数 + 右子数组的总逆序对数 + 左子数组和右子数组中交叉的逆序对数。
  3. 要求左(右)子数组的逆序对数我们可以使用递归的方法继续求,而左子数组和右子数组中交叉的逆序对数这个该怎么求呢?如果不施加任何条件,我们还是需要遍历左子数组中的元素,然后依次取右子数组中的元素进行对比,这样比较下来我们还是需要O(n2)的算法复杂度。
  4. 而我们知道如果经过归并排序后,左子数组和右子数组会分别变为有序的,那么我们在对比一组元素之后,如果发现逆序(左子数组中的元素比右子数组的元素大),那么右子数组的该元素会与左子树中后续所有元素构成逆序对。通过这个发现,我们可以利用归并排序算法代码,来求逆序对数,而该算法复杂度也优化到了和归并排序一致,为O(nlogn)。

我们取[53, 89][32, 45, 67]的归并来说明这个过程:

  1. 左子数组的53和右子数组的32对比,发现53大于32,那么32将和左子数组中53及其以后的元素构成逆序对,即[53, 32][89, 32]在这里插入图片描述
  2. 左子数组的53和右子数组的45对比,发现53大于45,那么45将和左子数组中53及其以后的元素构成逆序对,即[53, 45][89, 45]在这里插入图片描述
  3. 左子数组的53和右子数组的67对比,发现53小于45,则无逆序对。
    在这里插入图片描述
  4. 左子数组的89和右子数组的67对比,发现89大于67,那么67将和左子数组中89及其以后的元素构成逆序对,即[89, 67]
    在这里插入图片描述
  5. 右子数组中已无元素,则后续无逆序对。
    在这里插入图片描述
    累加每一次对比的结果: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

符合我们的预期。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值