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

leetcode 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 <= 105
  • -104 <= nums[i] <= 104

分析

这道题其实就是计算逆序对的变式。但是不是求所有逆序对的个数,而是求每一个数与之后面的数形成的逆序对的个数。
回忆一下求逆序对的方法:一个是用归并排序,一个是树状数组。这道题用树状数组还需要对数据进行一些处理。
我们这里着重讨论归并排序的方法,以及其各种实践与优化。

在最后我会把代码贴出来,但是不会进行讲解


我们先理解归并排序如何求每一个数的逆序对个数。
5 2 6 1为例
首先,我们对其进行分组:
在这里插入图片描述
再分别合并。
合并以5 2为例,如图左数组为5,右数组为2
在这里插入图片描述
然后我们在定义一个数组ret = []为合并后的数组
我们要先把小的数放进去,所以先判断left[0]是不是大于right[0],如果不大于,就把left[0]的放进去,反之则把right[0]放进去。

为什么是判断不大于?这值得深思。
简单分析一下,由于我们要找的是右侧严格小于当前数的个数。所以,遇到left[lindex] == right[rindex]的情况,应该优先放left中的数,才不会对右侧的数的数量造成影响。
我可能表达不是特别清晰,可以试着推导一下合并1 21 2 3的时候怎么做才正确。
如果有更清晰的解释方式可以在下面留言,造福所有人 ^_^

由于我们需要统计的是右边的比自己小的数,所以,应该在每次左数组的数被加入时更新它右侧比他小的数的个数
又因为判断使用的是不大于right数组已经被放进去的数一定比当前left中正在被放入的数小,所以我们只需要统计right数组被已经被放进去的个数就行了。
所以,此时,我们可以得到一个对应2: 05: 1(前面指数字,后面指在当前范围中右侧比自己小的数的个数)


同理,合并6 1可以得到1: 06: 1
这个时候,我们需要合并2 51 6。同理,在这一步,我们可以得到2: 15: 11: 06: 0
最后,再把每一个数在各个部分得到的对应加起来就得到了最终答案。

思路不错,但是遇到相同的数还能这么对应并合并吗?
其实解决方法很简单,我们不用数值本身来作为引索对应,我们通过这个数在原数组中的位置来对应,这样就不会出现重复的情况了,而且,引索在最后可以直接对应答案的数组,太棒了!


代码不要在意注释掉的调试代码

归并排序 - 最慢版
// 利用vector合并返回 
vector<int> & mergeSort(vector<int> & nums, int start, int end, vector<int> & smallCount) {
    // 只有一个元素直接返回
    // 返回它的下标,方便标记smallCount 
    if (start + 1 == end) {
        vector<int> ret(1, start);
        return ret;
    }
    
    vector<int> ret(end - start, 0);
    
    int mid = (start + end) / 2;
    
    // 我们采用左闭右开的写法 ^_^ 
    vector<int> left = mergeSort(nums, start, mid, smallCount);
    vector<int> right = mergeSort(nums, mid, end, smallCount);
    
    // printf("merge "); printVec(left); printf(" and "); printVec(right); putchar('\n');
    
    int li = 0, le = left.size(), ri = 0, re = right.size();
    int i = 0;
    while (li < le && ri < re) {
    	// 由于没有改变原数组,所以这里直接用下标对应原数组的值比较
    	// 但是放回数组的值任然是下标,而不是真正的值
        if (nums[left[li]] <= nums[right[ri]]) {
            smallCount[left[li]] += ri;
            ret[i++] = left[li++];
        } else {
            ret[i++] = right[ri++];
        }
    }
    
    while (ri < re) ret[i++] = right[ri++];
    // 无论是在哪里更新left,都需要加上ri
    // ri不仅是表示right数组当前的下标
    // 也表示比left[li]小且以前没有记录过的数的个数 
    while (li < le) smallCount[left[li]] += ri, ret[i++] = left[li++];
    
    // printf("\tto "); printVec(ret); putchar('\n');
    // printf("\tupdate smallCount to "); printVec(smallCount); putchar('\n');
    
    assert(i == end - start); // 这是用来判错的,可以删掉,没有影响
    
    return ret;
}

// 利用了函数重载的小技巧 
vector<int> mergeSort(vector<int> & nums, vector<int> & smallCount) {
    return mergeSort(nums, 0, nums.size(), smallCount);
}

这是最慢的版本,空间占用最多的版本,但是是最好理解的版本。
提交记录:慢
是不是很离谱。
分析:由于C++中vector会占用额外的空间,它的创建于销毁会产生损耗,所以会最慢。
解决:换成c原生的malloc不就好了吗?

归并排序 - 第二版
// 使用vector会比int*慢,而且内存占用更多 
// smallCount记录右侧严格比自己小的数 
int * mergeSort(vector<int> & nums, int start, int end, vector<int> & smallCount) {
	int * ret = (int *)malloc(sizeof(int) * (end - start));
	// 只有一个元素直接返回
	// 返回它的下标,方便标记smallCount 
	if (start + 1 == end) {
		ret[0] = start;
		return ret;
	}
	
	int mid = (start + end) / 2;
	
	// 我们采用左闭右开的写法 ^_^ 
	int * left = mergeSort(nums, start, mid, smallCount);
	int * right = mergeSort(nums, mid, end, smallCount);
	
	int li = 0, le = mid - start, ri = 0, re = end - mid;
	int i = 0, ie = end - start;
	while (li < le && ri < re) {
		if (nums[left[li]] <= nums[right[ri]]) {
			smallCount[left[li]] += ri;
			ret[i++] = left[li++];
		} else {
			ret[i++] = right[ri++];
		}
	}
	
	while (ri < re) ret[i++] = right[ri++];
	// 无论是在哪里更新left,都需要加上ri
	// ri不仅是表示right数组当前的下标
	// 也表示比left[li]小且以前没有记录过的数的个数 
	while (li < le) smallCount[left[li]] += ri, ret[i++] = left[li++];

	// 释放内存空间	
	free(left); free(right);
	
	return ret;
}

// 利用了函数重载的小技巧 
vector<int> mergeSort(vector<int> & nums, vector<int> & smallCount) {
	int * sorted = mergeSort(nums, 0, nums.size(), smallCount);
	// 这个地方是为了方便调试遍历输出才转换为vector
	vector<int> ret(sorted, sorted + nums.size());
	// 最后要记得释放,内存泄漏了多不好
	free(sorted);
	return ret;
}

这里其实没有太大的影响,还是很慢,空间并没有质上的提升
提交记录:还是慢
分析:代码思路没有变,还是创建新的数组来合并,获取与释放空间的损耗还是很大,而且每一个数都要在log N个数组中存在,这空间,想不小都难
解决:既然要降低空间操作次数,那么我们可以直接创一个临时数组,之后就全部用它来操作

归并排序 - 第三版
void mergeSort(vector<int> & nums, vector<int> & smallCount, int * indexes, int start, int end, int * tmp) {
	if (start + 1 == end) {
		tmp[start] = start;
		return;
	}
	
	int mid = (start + end) / 2;
	
	mergeSort(nums, smallCount, indexes, start, mid, tmp);
	mergeSort(nums, smallCount, indexes, mid, end, tmp);
	
	// printf("combine left right:"); 
	// printVec(tmp, start, mid); printVec(tmp, mid, end);
	
	int li = start, le = mid, ri = mid, re = end, i = start, ie = end;
	
	while (li < le && ri < re) {
		if (nums[tmp[li]] <= nums[tmp[ri]]) {
			smallCount[tmp[li]] += ri - mid;
			indexes[i++] = tmp[li++];
		} else {
			indexes[i++] = tmp[ri++];
		}
	}
	
	while (ri < re) indexes[i++] = tmp[ri++];
	while (li < le) smallCount[tmp[li]] += ri - mid, indexes[i++] = tmp[li++];
	
	// 合并后的数组要放回tmp中,方便上一层继续合并
	for (i = start; i < ie; i++) {
		tmp[i] = indexes[i];
	}
	
	// printf("\n\tto "); printVec(tmp, start, end); putchar('\n'); 
}

int * mergeSort(vector<int> & nums, vector<int> & smallCount) {
	// 没有改变原数组, 所以用tmp 
	int * tmp = (int *)malloc(sizeof(int) * nums.size());
	int * indexes = (int *)malloc(sizeof(int) * nums.size());
	mergeSort(nums, smallCount, indexes, 0, nums.size(), tmp);
	// tmp是临时的,当然要释放啊
	free(tmp);
	return indexes;
}

这下就有了质的提升
提交记录:提交之后忘记截屏了在这里插入图片描述
分析:这空间的量和操作量都少了这么多,不快不行啊
优化:
没想到还能优化吧 ^_^
可以发现,如果是合并1 23 4 5时,我们还要重新赋值10次,判断9次。那我们为什么不直接省略呢?
所以在mergeSort两边之后,我们可以加这么两(一)行代码

// 优化
// 当 nums[tmp[le - 1]]已经小于等于nums[tmp[ri]]时
// 左边和右边已经是排好序的状态,就不需要再排了 
if (nums[tmp[le - 1]] <= nums[tmp[ri]])
	return;

这样,速度就提升了太多了
提交记录:超快

树状数组

最后,我还是弄一个树状数组的代码吧,懒得写分析了,自己看吧,这不是重点。

// +2防止超出上界 
const int MAXI = 2e4+2;
// +1是为了不让0出现,因为0-1为-1,使用update会出大问题 
const int ADDI = 1e4+1;

// Binary Index Tree
// 树状数组
class BIT {
private:
	int nums[MAXI] = {0}; 
public:
	int lowbit(int i) {
		return i & -i;
	}
	void update(int i, int k) {
		while (i < MAXI) {
			nums[i] += k;
			i += lowbit(i);
		}
	}
	int getsum(int i) {
		int ans = 0;
		while (i) {
			ans += nums[i];
			i -= lowbit(i);
		}
		return ans;
	}
};

class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
    	vector<int> smallCount(nums.size());
    	BIT bit;
    	// 找右侧,所以从右向左更新 
    	for (int i = nums.size() - 1; i >= 0; i--) {
    		int cn = nums[i] + ADDI;
    		// 使cn的数量 + 1 
    		bit.update(cn, 1);
    		// 设为比自己小的数的数量 
    		smallCount[i] = bit.getsum(cn - 1);
		}
		return smallCount; 
    }
};

这东西直接复制后交上去吧 *^O^*
提交记录:在这里插入图片描述
芜湖,起飞

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值