这一周完成了几道DFS的题目(Path Sum等),但主要还是用二分查找/树状数组解决了一道Hard的题:Count of Smaller Numbers After Self。
题目描述 :You are given an integer array nums and you have to return a new counts array. The counts array has the property where counts[i] is the number of smaller elements to the right of nums[i].
Example:
Given nums = [5, 2, 6, 1]
To the right of 5 there are 2 smaller elements (2 and 1).
To the right of 2 there is only 1 smaller element (1).
To the right of 6 there is 1 smaller element (1).
To the right of 1 there is 0 smaller element.
Return the array [2, 1, 1, 0].
分析:这个题目拿到的第一瞬间就想到的是做排序+二分查找。具体过程如下:
从右往左遍历数组nums
record数组为空,直接插入当前遍历的nums的元素,res[i] = 0;
record数组不为空,判断当前遍历的nums的元素应该插入哪一个位置(使用二分查找),比如如果遍历到2,record中已经有了元素1和6,2应该插入1和6中间,位置为1,它前面有一个元素1,那么就说明在nums数组里面,在元素2后面比它小的元素,有一个,即res[i] = 1;
用插入元素获得其相应位置的方式,在这个位置以前有多少个元素,就对应了在nums数组中在它后面比它小的元素个数。
结束遍历,返回res数组
这个方法的思路清晰简单,实现起来也比较简单,代码如下:
class Solution {
public:
// 二分查找过程
int binarysearch(vector<int> & input, int begin, int end,int idx){
if(input[end] < idx)
return end+1;
while(begin <= end){
int mid = begin + (end-begin)/2;
if(input[mid] < idx)
begin = mid + 1;
else
end = mid - 1;
}
return begin;
}
vector<int> countSmaller(vector<int>& nums) {
vector<int> res;
if(nums.empty()) return res;
res = nums;
res[nums.size() - 1] = 0;
vector<int>record(nums.size(),0);
record[0] = nums[nums.size()-1];
int len = 0;
for(int i = nums.size()-2; i >= 0; i--){
int pos = binarysearch(record,0,len++,nums[i]);
// 将元素插入指定位置
record.insert(record.begin()+pos,nums[i]);
res[i] = pos;
}
return res;
}
};
二分查找的时间复杂度是 O(logn) ,而vector的insert函数的复杂度是 O(n) ,所以上述代码的时间复杂度是 O(n2) 。通过RunTime可以看出这个算法跑了136ms,还是很慢的,所以又有了优化的想法。通过查找资料可以发现,要优化这个算法,要用到树状数组(Binary Indexed Tree)。
树状数组是线段树的一种,区别在于线段树是完全二叉树,而树状数组不是。树状数组的优势在于,其做修改和求和都在 O(logn) 中。
1、什么是树状数组
树状数组是一种查询和求和的数据结构,可以很方便的修改和查询一段区间之间的元素和。具体的构造过程如下图所示。
在这里可以看到,这棵树的高度不会超过 logn 。假设原数组为A,树状数组为C,那么有:
我们可以看到,类似与 C4 , C8 , C16 ,可以表示前4、前8、前16项的和,而其他的位置的前N项和,可以由对C数组的累加得到,比如前15项的和可以写作A[15] + C[8]+C[12]+C[14]得到,这遵循一个什么规则呢?主要就是利用了一个lowbit的概念。
lowbit (i) 是指i的2的幂方和展开式中的最小幂次,比如说7= 22+21+20 ,那么lowbit(7) = 1;也可以把lowbit(i)看作是i的二进制表示字符串中,从右边数过来的1的位置(111中最右边的1的位置就是1)。为什么要介绍lowbit,这是因为我们在构造树状数组时,一个元素会加入到哪一层的结点计算,跟其lowbit的值,是息息相关的。如下表所示。而对于每一个子结点的父结点,其下标 p 可以通过 p=i+lowbit(i) 计算得到。
关于树状数组,刚刚也提到了其最大的特点就是修改和求和,如果我们修改了某一个结点,通过上述的公式可以不断的找到父结点的下标,进而可以可以不断的将修改向上层推进,所以修改的复杂度最坏情况也就是 O(logn) 。
而对于求和,通过不断的回溯到这一个结点之前的更高层的父结点,把这些父结点和这个结点的值累加,就可以完成求和,而这个结点的之前的更高阶的父结点,则可以通过 p=i−lowbit(i) 得到,比如对于前10项的和,除了取C[10]外,还要取C[8(=10-lowbit(10))],因为8再往前也没有其他的父结点了( 8−lowbit(8)=0 ),所以sum = C[10]+C[8],计算完毕,其时间复杂度最坏也是 O(logn) 。
2、怎么用树状数组解决上述的Count of Smaller Numbers After Self问题
关于上述的Count of Smaller Numbers After Self问题,通过树状数组解决,可以把时间复杂度从 O(n2) 变为 O(logn) 。
定义一个树状数组,其对应的原数组的A的值全为0,那么C的值也是0;
对原先的数组nums进行排序,得到一个排序后的数组,并计算出原数组中每个元素在排序后的位置。
对于每个原先数组的元素,从后往前遍历。我们取每一个元素排序后的位置,并将这个位置看作它要插入树状数组中的位置,之后把数组中这个元素的前面存在的元素个数累计,就可以得到的在它前面有多少个值。计算得到后,我们就把对应位置的结点的值变为从0变为1,这样下一次如果有元素比当前元素大,在计算元素的个数时,这个元素就会被考虑进去;未改变的对应位置的值就表示其插入数组时前面没有比它小的值存在。
通过分析可以得到,关于树状数组的实现以及这个问题的实现代码如下:
class BIT{
int N;
vector<int>tree; // 在这里我们把树状数组的下标从1开始表示
public:
BIT(int N):N(N),tree(N+1,0){}
// 得到x用2的幂方和展开式的最小幂次
int lowbit(int x){return x&(-x);}
// 修改pos位置上的值,并修改其相应的父结点的值
void update(int pos,int change){
while(pos < N){
tree[pos] += change;
// 父结点的下标
pos += lowbit(pos);
}
}
// 求前M项的和
int getsum(int M){
int sum = 0;
while( M > 0 ){
sum += tree[M];
M -= lowbit(M);
}
return sum;
}
};
class Solution{
public:
vector<int> countSmaller(vector<int> & nums){
vector<int> res;
if(nums.empty()) return res;
int len = nums.size();
res = nums;
// 先对原数组排序并去重
vector<int> tmp = nums;
sort(tmp.begin(),tmp.end());
unique(tmp.begin(), tmp.end());
// 求出原数组中每个元素到排序后数组中的位置
map<int,int>reflect;
for(int i = 0; i < tmp.size(); i++)
reflect[tmp[i]] = i+1;// +1的原因是因为我的树状数组时从1开始取下标
BIT bit_tree(len+1);
for(int i = len-1; i >= 0; i--)
{
// 看这个元素的位置前面有几个元素了
res[i] = bit_tree.getsum(reflect[nums[i]] - 1);
// 更新这个位置的值
bit_tree.update(reflect[nums[i]],1);
}
return res;
}
};
通过RunTime可以得到,此时的算法只需要33ms就可以编译结束,大大的得到了优化。