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 2
和1 2 3
的时候怎么做才正确。
如果有更清晰的解释方式可以在下面留言,造福所有人 ^_^
由于我们需要统计的是右边的比自己小的数,所以,应该在每次左数组的数被加入时更新它右侧比他小的数的个数
又因为判断使用的是不大于
,right
数组已经被放进去的数一定比当前left
中正在被放入的数小,所以我们只需要统计right
数组被已经被放进去的个数就行了。
所以,此时,我们可以得到一个对应2: 0
,5: 1
(前面指数字,后面指在当前范围中右侧比自己小的数的个数)
同理,合并6 1
可以得到1: 0
, 6: 1
。
这个时候,我们需要合并2 5
和1 6
。同理,在这一步,我们可以得到2: 1
,5: 1
,1: 0
,6: 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 2
和3 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^*
提交记录:
芜湖,起飞