题目
315. 计算右侧小于当前元素的个数
思路总结
没有想出时间复杂度比n2的方法。外循环遍历一遍所有数组元素需要n次循环,内循环累计所有比当前遍历元素小的元素最坏情况也要循环n次。
以下是官方解答和一位网友的思路。
官方思路
从后往前循环遍历的过程中,把当前遍历的元素nums[i]对应的桶中的值+1,同时count[i]的值就是目前比nums[i]小的元素的桶中值之和。
主要需要解决两个问题:
1.nums数组中的取值不确定,如何为每个存在的值分配唯一的桶,即“离散化”。离散化的方法有很多,但是目的是一样的,即把原序列的值域映射到一个连续的整数区间,并保证它们的偏序关系不变。
2.动态维护前缀和(插入和查询),使两者操作的时间复杂度小于n。
第一点的解决方法可以是把nums数组去重排序,已知元素值,通过二分法来获取去重数组中对应的下标。已知下标访问去重数组得到元素值。(个人理解是这个去重数组就是一个映射工具)
第二点可以利用树状数组来解决。
网友思路
逆序遍历nums数组的所有元素,遍历到新的元素时,把它插入到一个有序链表或数组中(按从小到大排序)。那么插入时在新链表或数组中的序号就是比该元素小的元素个数。
但是好像这样复杂度始终是n2。因为如果插入到链表,那么插入时需要从头节点依次访问到合适的位置时间复杂度是n;如果插入到数组,在插入后,新插入元素之后的所有元素需要被复制移位一次,时间复杂度也是n。
实现
class Solution { private: vector<int> c; vector<int> a; void Init(int length) { c.resize(length, 0); } int LowBit(int x) { return x & (-x); } void Update(int pos) { while (pos < c.size()) { c[pos] += 1; pos += LowBit(pos); } } int Query(int pos) { int ret = 0; while (pos > 0) { ret += c[pos]; pos -= LowBit(pos); } return ret; } void Discretization(vector<int>& nums) { a.assign(nums.begin(), nums.end()); sort(a.begin(), a.end()); a.erase(unique(a.begin(), a.end()), a.end()); } int getId(int x) { return lower_bound(a.begin(), a.end(), x) - a.begin() + 1; } public: vector<int> countSmaller(vector<int>& nums) { vector<int> resultList; Discretization(nums); Init(nums.size() + 5); for (int i = (int)nums.size() - 1; i >= 0; --i) { int id = getId(nums[i]); resultList.push_back(Query(id - 1)); Update(id); } reverse(resultList.begin(), resultList.end()); return resultList; } }; /**作者:LeetCode-Solution 链接:https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/solution/ji-suan-you-ce-xiao-yu-dang-qian-yuan-su-de-ge-s-7/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。*/
拓展学习
树状数组
解决的问题
我们希望借助树状数组平衡了查询和更新的复杂度。
在上面讲的寻常方法中,1次更新或查询,要么复杂度为1要么为n。如果我们把前缀和分解为多个不同的2的幂的累加,那么在更新或者查询时,复杂度都变成了logn。
具体分解见下图。举例来说:如果查询1到11前缀累加和,那么只需要累加S8、S10、S11。如果要更新元素5的值,那么只需要更新S5、S6、S8、S16。
已知要更新的元素值,或者查询前缀长度,如何计算要被更新或者访问的数组下标?
已知查询前缀长度k,求前缀和。通过上面图示可以看到通过不断对二进制末尾的1进行取反操作可以得到下一次累加的下标。比如求S11,11的二进制编码是1011。那么需要累加的是C11(1011),C10(1010),C8(1000)。
一个高效而巧妙的方法lowbit(int m),作用是求出二进制数末尾为1的位置。为什么这样可以?因为负数在计算机中用补码存储,也就是对应正数所有位取反再+1。(0取反加1还是0,1取反加1还是1)
int lowbit(int m){ return m&(-m); }
求前缀和的代码可以如下
int ans = 0; int getSum(int m){ while(m > 0){ ans += C[m]; m -= lowbit(m); } }
这样一来单次求前缀和的时间复杂度是logn。
那已知被更新的元素下标,如何更新辅助数组呢?即同时被更新的元素的下标是什么?
通过上面的图也可以看出,通过不断对二进制数末尾1的位置+1即得到下一个要更新的辅助数组中的下标(直到达到最大值n)。
更新元素值的代码如下
void update(int x, int value){ A[x] += value; //不能忘了对A数组进行维护,尽善尽美嘛 while(x <= n){ C[x] += value; x += lowbit(x); } }
这里的时间复杂度同样是logn。