Offer必备算法10_分治归并_四道力扣题详解(由易到难)

目录

①力扣912. 排序数组

解析代码

②力扣LCR 170. 交易逆序对的总数

解析代码1

解析代码2

③力扣315. 计算右侧小于当前元素的个数

解析代码

④力扣493. 翻转对

解析代码


①力扣912. 排序数组

912. 排序数组

难度 中等

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 10^4
  • -5 * 10^4 <= nums[i] <= 5 * 10^4
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {

};

解析代码

还是这题,用归并写一下,且用了全局数组提高效率,否则每一次递归都要开数组:

class Solution {
    vector<int> tmp;
public:
    vector<int> sortArray(vector<int>& nums) {
        tmp.resize(nums.size());
        mergeSort(nums, 0, nums.size() - 1);
        return nums;
    }

    void mergeSort(vector<int>& nums, int left, int right)
    {
        if(left >= right)
            return;
        // 选择中间点划分两个区间并排序 [left, mid] [mid + 1, right]
        int mid = (left + right) >> 1;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        // 合并两个有序数组,i是全局数组tmp下标
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] < nums[cur2] ? nums[cur1++] : nums[cur2++];
        // 处理没遍历完的数组,只有一个while可进
        while(cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while(cur2 <= right)
            tmp[i++] = nums[cur2++];
        // 还原数组
        for(int j = left; j <= right; ++j)
            nums[j] = tmp[j - left];
    }
};

②力扣LCR 170. 交易逆序对的总数

LCR 170. 交易逆序对的总数

 难度 困难

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。

示例 1:

输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

限制:

0 <= record.length <= 50000

class Solution {
public:
    int reversePairs(vector<int>& record) {

};

解析代码1

暴力枚举会超时

        用归并排序求逆序数是很经典的方法,主要就是在归并排序的合并过程中统计出逆序对的数量,也就是在合并两个有序序列的过程中,能够快速求出逆序对的数量。

如果将数组从中间划分成两个部分,那么我们可以将逆序对产生方式划分成三组:

  • 逆序对中两个元素:全部从左数组中选择。
  • 逆序对中两个元素:全部从右数组中选择。
  • 逆序对中两个元素:一个选左数组另一个选右数组。

        根据排列组合的分类相加原理,三种情况下产生的逆序对的总和,正好等于总的逆序对数量。而这个思路正好匹配归并排序的过程:

  • 先排序左数组。
  • 再排序右数组。
  • 左数组和右数组合二为一。

        因此,我们可以利用归并排序的过程,先求出左半数组中逆序对的数量,再求出右半数组中逆序对的数量,最后求出⼀个选择左边,另⼀个选择右边情况下逆序对的数量,三者相加即可。

class Solution {
    vector<int> tmp;
public:
    int reversePairs(vector<int>& record) {
        tmp.resize(record.size());
        return mergeSortCnt(record, 0, record.size() - 1);
    }

    int mergeSortCnt(vector<int>& arr, int left, int right)
    {   // 解法一:找出该数之前,有多少个数比我大 -> 升序
        if(left >= right)
            return 0;
        int ret = 0, mid = (left + right) >> 1;
        // 左边逆序对的个数+排序 + 右边逆序对的个数+排序
        ret += mergeSortCnt(arr, left, mid);
        ret += mergeSortCnt(arr, mid + 1, right);
        // 一左一右逆序对的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
        {
            if(arr[cur1] > arr[cur2])
            {   // 此时cur2后面的都是比cur1小的
                ret += mid - cur1 + 1;
                tmp[i++] = arr[cur2++];
            }
            else
            {
                tmp[i++] = arr[cur1++];
            }
        }
        // 处理排序
		while (cur1 <= mid) 
            tmp[i++] = arr[cur1++];
		while (cur2 <= right) 
            tmp[i++] = arr[cur2++];
		for (int j = left; j <= right; j++)
			arr[j] = tmp[j - left];
        
        return ret;
    }
};

解析代码2

在代码一的基础上可以选择找出该数之前,有多少个数比我小 -> 降序:

class Solution {
    vector<int> tmp;
public:
    int reversePairs(vector<int>& record) {
        tmp.resize(record.size());
        return mergeSortCnt(record, 0, record.size() - 1);
    }

    int mergeSortCnt(vector<int>& arr, int left, int right)
    {   // 解法二:找出该数之前,有多少个数比我小 -> 降序
        if(left >= right)
            return 0;
        int ret = 0, mid = (left + right) >> 1;
        // 左边逆序对的个数+排序 + 右边逆序对的个数+排序
        ret += mergeSortCnt(arr, left, mid);
        ret += mergeSortCnt(arr, mid + 1, right);
        // 一左一右逆序对的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
        {
            if(arr[cur1] > arr[cur2])
            {   // 此时cur2后面的都是比cur1小的
                // ret += mid - cur1 + 1;
                // tmp[i++] = arr[cur2++];

                ret += right - cur2 + 1;
                tmp[i++] = arr[cur1++]; // 降序
            }
            else
            {
                // tmp[i++] = arr[cur1++];
                tmp[i++] = arr[cur2++]; // 降序
            }
        }
        // 处理排序
		while (cur1 <= mid) 
            tmp[i++] = arr[cur1++];
		while (cur2 <= right) 
            tmp[i++] = arr[cur2++];
		for (int j = left; j <= right; j++)
			arr[j] = tmp[j - left];
        
        return ret;
    }
};

③力扣315. 计算右侧小于当前元素的个数

315. 计算右侧小于当前元素的个数

难度 困难

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

示例 2:

输入:nums = [-1]
输出:[0]

示例 3:

输入:nums = [-1,-1]
输出:[0,0]

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
class Solution {
public:
	vector<int> countSmaller(vector<int>& nums) {

};

解析代码

暴力枚举会超时

        这道题的解法与 求数组中的逆序对 的解法是类似的,但是这⼀道题要求的不是求总的个数,而是要返回⼀个数组,记录每⼀个元素的右边有多少个元素比自己小。

        但是在我们归并排序的过程中,元素的下标是会跟着变化的,因此我们需要⼀个辅助数组,来将数组元素和对应的下标绑定在⼀起归并,也就是再归并元素的时候,顺势将下标也转移到对应的位置上。

        由于要快速统计出某⼀个元素后面有多少个比它小,因此可以利用求逆序对的降序方法。

class Solution {
	vector<int> ret; // 要返回的数组
	vector<int> index; // nums中元素的原始下标
	vector<int> tmp_nums;
	vector<int> tmp_index; // 和nums里的元素绑定一起移动
public:
	vector<int> countSmaller(vector<int>& nums) {
		int n = nums.size();
		ret.resize(n);
		index.resize(n);
		tmp_nums.resize(n);
		tmp_index.resize(n);
		for (int i = 0; i < n; ++i)
		{
            index[i] = i; // 初始化下标数组
        }
		mergeSortCnt(nums, 0, n - 1);
		return ret;
	}

	void mergeSortCnt(vector<int>& nums, int left, int right)
	{
		if (left >= right)
			return;
		int mid = (left + right) >> 1;
		mergeSortCnt(nums, left, mid);
		mergeSortCnt(nums, mid + 1, right);

		int cur1 = left, cur2 = mid + 1, i = 0;
		while (cur1 <= mid && cur2 <= right)
		{
			if (nums[cur1] > nums[cur2])
			{
				ret[index[cur1]] += (right - cur2 + 1);
				tmp_nums[i] = nums[cur1];
				tmp_index[i++] = index[cur1++];
			}
			else
			{
				tmp_nums[i] = nums[cur2];
				tmp_index[i++] = index[cur2++];
			}
		}
		while (cur1 <= mid)
		{
			tmp_nums[i] = nums[cur1];
			tmp_index[i++] = index[cur1++];
		}
		while (cur2 <= right)
		{
			tmp_nums[i] = nums[cur2];
			tmp_index[i++] = index[cur2++];
		}
		for (int j = left; j <= right; ++j)
		{
			nums[j] = tmp_nums[j - left];
			index[j] = tmp_index[j - left];
		}
	}
};

④力扣493. 翻转对

493. 翻转对

难度 困难

给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

示例 1:

输入: [1,3,2,3,1]
输出: 2

示例 2:

输入: [2,4,3,5,1]
输出: 3

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。
class Solution {
public:
    int reversePairs(vector<int>& nums) {

    }
};

解析代码

        大思路与求逆序对的思路⼀样,就是利用归并排序的思想,将求整个数组的翻转对的数量,转换成三部分:

  • 左半区间翻转对的数量。
  • 右半区间翻转对的数量。
  • 一左一右选择时翻转对的数量。

        重点就是在合并区间过程中,如何计算出翻转对的数量。 与上个问题不同的是,上一道题我们可以一边合并一遍计算,但是这道题要求的是左边元素大于右边元素的两倍,如果我们直接合并的话,是无法快速计算出翻转对的数量的。

        例如 left = [4, 5, 6] right = [3, 4, 5] 时,如果是归并排序的话,需要计算 left 数组中有多少个 能与 3 组成翻转对。但是我们要遍历到最后一个元素 6 才能确定,时间复杂度较高。 因此需要在归并排序之前完成翻转对的统计。因为左右区间是有序的,所以使用双指针统计。

这里用升序(也可以用降序):

class Solution {
    vector<int> tmp;
public:
    int reversePairs(vector<int>& nums) {
        tmp.resize(nums.size());
        return mergeSortCnt(nums, 0, nums.size() - 1);
    }

    int mergeSortCnt(vector<int>& nums, int left, int right)
    {   // 解法一:升序
        if(left >= right)
            return 0;
        int ret = 0, mid = (left + right) >> 1;
        // 先计算左右两侧翻转对数量
        ret += mergeSortCnt(nums, left, mid);
        ret += mergeSortCnt(nums, mid + 1, right);

        // 计算翻转对数量
        int cur1 = left, cur2 = mid + 1, i = left;
        while(cur2 <= right) // 升序的情况
        {
            while(cur1 <= mid && nums[cur2] >= nums[cur1] / 2.0)
            {
                cur1++;
            }
            if(cur1 > mid)
                break;
            ret += mid - cur1 + 1;
            cur2++;
        }

        // 合并两个有序数组
        cur1 = left, cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
        
		while (cur1 <= mid) 
            tmp[i++] = nums[cur1++];
		while (cur2 <= right) 
            tmp[i++] = nums[cur2++];
		for (int j = left; j <= right; j++)
			nums[j] = tmp[j];
        
        return ret;
    }
};
  • 49
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值