[剑指-Offer] 51. 数组中的逆序对(思维、归并排序、巧妙解法)

1. 题目来源

链接:数组中的逆序对
来源:LeetCode——《剑指-Offer》专项

2. 题目说明

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

限制:

  • 0 <= 数组长度 <= 50000

3. 题目解析

方法一:TLE+暴力+常规解法

暴力解法首先试试,虽然肯定 AC 不了。顺序遍历数组元素,拿到一位数组元素后向后遍历剩余的所有数组元素,比较大小,计数即可。时间复杂度 O ( n 2 ) O(n^2) ,空间复杂度 O ( 1 ) O(1)

参见代码如下:

// TLE
// 35 / 139 个通过测试用例

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int cnt = 0, tmp = 0;;
        for (int i = 0; i < nums.size(); ++i) {
            tmp = nums[i];
            for (int j = i; j < nums.size(); ++j) {
                if (tmp > nums[j]) ++cnt;
            }
        }
        return cnt;    
    }
};

方法二:思维+归并排序+巧妙解法

来自《剑指-Offer》的解法,利用 归并排序 的思想解题。以数组 {7, 5, 6, 4} 为例来分析统计逆序对的过程。每次扫描到一个数字的时候,我们不能拿它和后面的每一个数字作比较,否则时间复杂度就是 O ( n 2 ) O(n^2) ,因此我们可以考虑先比较两个相邻的数字。

在这里插入图片描述
上图中省略了最后一步,即复制第二个子数组最后剩余的 4 到辅助数组中。

  • ( a ) P1 指向的数字大于 P2 指向的数字,表明数组中存在逆序对。P2 指向的数字是第二个子数组的第二个数字,因此第二个子数组中有两个
    数字比 7 小。把逆序对数目加 2,并把 7 复制到辅助数组,向前移动 P1P3
  • ( b ) P1 指向的数字小于 P2 指向的数字,没有逆序对。把 P2 指向的数字复制到辅助数组,并向前移动 P2P3
  • ( c ) P1 指向的数字大于 P2 指向的数字,因此存在逆序对。由于 P2 指向的数字是第二个子数组的第一个数字,子数组中只有一个数字比 5小。把逆序对数目加 1,并把 5 复制到辅助
    数组,向前移动 P1P3

接下来我们统计两个长度为 2 的子数组之间的逆序对。在第二张图片中细分第一张图片的步骤 (d) 的合并子数组及统计逆序对的过程:

  • 我们先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。
    • 如果第一个子数组中的数字大于第二个子数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数(如第二张图 (a) 和第二张图 (c) 所示)
    • 如果第一个数组中的数字小于或等于第二个数组中的数字,则不构成逆序对(如第二张图 (b) 所示)
  • 每一次比较的时候,都把较大的数字从后往前复制到一个辅助数组中去,确保辅助数组中的数字是递增排序的。在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。

经过前面详细的讨论,可以总结出统计逆序对的过程:

  • 先把数组分隔成子数组,先统计出子数组内部的逆序对的数目
  • 再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。

如果对排序算法很熟悉,我们不难发现这个排序的过程实际上就是归并排序。关于归并排序,博主有相关总结,比较详细,推荐参考:[排序算法] 9. 归并排序递归与非递归实现及算法复杂度分析(分治算法、归并排序、复杂度分析)

故此,我们可以基于归并排序写代码了。

参见代码如下:

// 执行用时 :144 ms, 在所有 C++ 提交中击败了94.34%的用户
// 内存消耗 :47.3 MB, 在所有 C++ 提交中击败了100.00%的用户

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        vector<int> vt(nums);
        int cnt = help(nums, vt, 0, nums.size() - 1);
        return cnt;
    }

    int help(vector<int>& nums, vector<int>& vt, int left, int right) {
        if (left == right) {
            vt[left] = nums[left];
            return 0;
        }
        int len = (right - left) / 2;
        int start = help(vt, nums, left, left + len);
        int end = help(vt, nums, left + len + 1, right);
        int i = left + len;
        int j = right;
        int index_vt = right;
        int cnt = 0;
        while (i >= left and j >= left + len + 1) {
            if (nums[i] > nums[j]) {
                vt[index_vt--] = nums[i--];
                cnt += j - left - len;
            } 
            else {
                vt[index_vt--] = nums[j--];
            }
        }
        for (; i >= left; --i) vt[index_vt--] = nums[i];
        for (; j >= left + len + 1; --j) vt[index_vt--] = nums[j];

        return start + end + cnt;
    }
};
发布了332 篇原创文章 · 获赞 167 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/yl_puyu/article/details/104727219