引发我对逆序对这个问题思考的源自这道题:315. Count of Smaller Numbers After Self这道题要找出某个元素之后出现的比这个元素小的元素个数:ex. Given[5, 2, 6, 1], return [2, 1, 1, 0]
这道题只是求逆序对个数的另外一个变形而已,所以这篇就着重说一下数组中逆序对个数的四种求法。
归并方法
以前我以为求一个序列逆序对个数的解法就是归并排序的改版。归并解法:
因为要求每个元素的逆序对个数,所以一下解法的相当一部分花在来找原数组的对应关系上面。
vector<int> res;
void mergesort(vector<II>& nums,int left,int right){
if(left>= right)
return;
int mid = (left+right)/2;
mergesort(nums,left,mid);
mergesort(nums,mid+1,right);
vector<II> leftnums(mid-left+1),rightnums(right-mid);
for(int i = left;i<=mid;i++)
leftnums[i-left] = nums[i];
for(int i = mid+1;i<=right;i++)
rightnums[i-mid-1] = nums[i];
leftnums.push_back(II(INT_MAX,-1));
rightnums.push_back(II(INT_MAX,-1));
int i = 0,j=0,k = left;
int mark = 0;
while(i<leftnums.size()-1||j<rightnums.size()-1){
if(leftnums[i]<=rightnums[j]){
res[leftnums[i].second] += mark;
nums[k++] = leftnums[i++];
}else{
mark++;
nums[k++] = rightnums[j++];
}
}
return;
}
vector<int> countSmaller(vector<int>& nums) {
res = vector<int>(nums.size(),0);
vector<II> new_nums(nums.size());
for(int i = 0;i<nums.size();i++)
new_nums[i] = II(nums[i],i);
mergesort(new_nums,0,nums.size()-1);
return res;
}
BST(二叉搜索树)
先说一个额外的变形题型:
《程序员面试金典》上面的题目:P77 11.8题:读取一串整数流,要求尽可能短的时间内,返回已读取的数字流中,比x小的元素个数。设计
void add(int x)和int getRankOfNumber(int x)两个函数接口。
如果不是一个数字流的话,就直接将这个数组排序,然后每个元素排名出现的位置,就是整个数组中比其小的元素个数。
但是如果处在流中,则每次排序花费太大,此时需要一个数组结构能够完成这个工作。所以此时就想到二叉树是一个非常好的数据结构。【一般需要用到数据结构的问题中,就想一下:堆,二叉树,栈,队列这四个方面想就八九不离十了】
所以弄一个BST,每个节点中额外记录一下,这个节点左子树的元素个数LeftNum。
getRankOfNumber(int x)函数
就沿着BST数去查找这个元素所在的数节点,在查找的途中,如果要往左子树查找的话,就不做改变,如果要往右子树查找的话,即当前节点和其左子树所有节点都比它小,所以res
+= LeftNum+1
add(int x)函数
就是一个经典的BST插入,不过当其要插入当前节点的左子树节点中时,就将当前节点的LeftNum值加一即可。
再回到这道题本身,其实当时第一眼看到这道题的时候,我并没有往逆序对这种情况上去想,而是直接就参照上面那题的思路用二叉树去做了,二叉树的解法就是,从右往左一次扫描这个二叉树,进行建树,在插入节点的时候,就直接找出当前情况下它的rank值存到数组中即可。
我的代码如下:
struct TreeNode {
int val;
int count;
TreeNode *left;
TreeNode *right;
TreeNode(int x,int c):val(x),count(c),left(NULL), right(NULL) {}
};
vector<int> countSmaller(vector<int>& nums) {
vector<int> res(nums.size(),0);
if(nums.size() == 0)
return res;
TreeNode *root = new TreeNode(nums[nums.size()-1],0);
res[nums.size()-1] = 0;
for(int i = nums.size()-2;i>=0;i--){
TreeNode *father ;
TreeNode *now = root;
int sum = 0;
while(now != NULL){
father = now;
//对于这种判断大小的问题,一定要注意等于的情况怎么处理,这道题中当其相等的话,就将其放到左子树中,是一个合理的做法。因为往右边走的话,需要加上这个数本身的所有左节点个数,与题意不符。
if(nums[i]<=now->val){
now->count++;
now = now->left;
if(now == NULL){
father->left = new TreeNode(nums[i],0);
break;
}
}else{
sum += now->count+1;
now = now->right;
if(now == NULL){
father->right = new TreeNode(nums[i],0);
break;
}
}
}
res[i] = sum;
}
return res;
}
一如上面注释中所说的,其实这种比较大小的,或者涉及到类似于排序类的问题,一定要注意等于的情况怎么处理的。之前一篇Blog:快速排序程序及易错点总结提到的也是等于情况的处理问题。
树状数组
对于树状数组我看的一个还不错的参考资料:
数据结构之树状数组是北理ACM的课件
简单来说树状数组是一个数据结构,它能高效( O(logN) )的复杂度下,完成计算a[i~j]元素和的任务。所以先将原数组给离散化成1~n之间的数,然后将其从后到前,依次放到这个树状数组中,然后每次放进去之前,先计算从1~n中有多少个元素了。
int lowbit(int x){ return x&(-x);}
int getsum(int x){
int sum = 0;
while(x>0){
sum += c[x];
x = x-lowbit(x);
}
return sum;
}
void update(int x,int val){
while(x<c.size()){
c[x] += val;
x = x+lowbit(x);
}
}
vector<int> countSmaller(vector<int>& nums) {
vector<int> res = vector<int>(nums.size(),0);
vector<II> new_nums(nums.size());
vector<int> n(nums.size());
c = vector<int>(nums.size()+1,0);
for(int i = 0 ;i<nums.size();i++)
new_nums[i] = II(nums[i],i);
//注意这个地方的sort能够处理重复元素的情况,因为pair进行比较的时候是先比的first再比的second,所以此时即使5,5,5这样的序列,在排序的时候稳定性是不变的,最后结果会是{1,2,3}这样的大小顺序
sort(new_nums.begin(),new_nums.end());
for(int i = 0;i<nums.size();i++)
n[new_nums[i].second] = i+1;
for(int i = n.size()-1;i>=0;i--){
res[i] = getsum(n[i]);
update(n[i],1);
}
return res;
}
今天看了一个大神的解法,更是惊为天人,直接用map/unordered_map去做的这道题,就免去了序列化的步骤了。
unordered_map<long long,int> c;
long long int lowbit(long long int x){ return x&(-x);}
int getsum(long long int x){
int sum = 0;
//因为x可能为负值所以需要将其加上2^32
x += (1ll<<32);
while(x>0){
sum += c[x];
x = x-lowbit(x);
}
return sum;
}
void update(long long int x,int val){
x += (1ll<<32);
//其实此处最好的办法是x < (1ll<<32)+max(nums)
while(x<(1ll<<34)){
c[x] += val;
x = x+lowbit(x);
}
}
vector<int> countSmaller(vector<int>& nums) {
vector<int> res = vector<int>(nums.size(),0);
for(int i = nums.size()-1;i>=0;i--){
//注意这个地方找nums[i]-1ll来去处理相等的情况,实在是精妙啊
res[i] = getsum((long long)nums[i]-1ll);
update(nums[i],1);
}
return res;
}
上面的写法更加简洁,因为不用序列化了。但是其实时间复杂度会更高一些。(当然如果nums里面的元素足够的多,上面这种方法反而会更加快速)
其实呢,又做了下面那道题493. Reverse Pairs,这道题用离散化的方法就不行了,因为要统计的是nums[i]<2*nums[j]的个数,如果再做离散化的话其序关系会发生变化的。
线段树解法
线段树我也是第一次写,中间写的也是感觉坑点多多,详细的见我下篇博文吧。这个只记录一下程序怎么写。
typedef pair<int, int> II;
struct segtree{
int left;
int right;
int count;
segtree() :left(0), right(0), count(0){
}
};
vector<segtree> tree;
void buildtree(int root, int left, int right){
tree[root].left = left;
tree[root].right = right;
tree[root].count = 0;
if (left == right)
return;
int mid = (left + right) >> 1;
buildtree(root << 1, left, mid);
buildtree(root << 1 |1, mid + 1, right);
}
void update(int left, int right, int root, int value){
tree[root].count += value;
if (tree[root].left == tree[root].right)
return;
int mid = (tree[root].left + tree[root].right) >> 1;
if (right <= mid)
update(left, right, root << 1, value);
else
if (left>mid)
update(left, right, root << 1|1, value);
else{
update(left, mid, root << 1, value);
update(mid + 1, right, root << 1 |1, value);
}
}
int query(int left, int right, int root){
if (left == tree[root].left&&right == tree[root].right)
return tree[root].count;
int mid = (tree[root].left + tree[root].right) >> 1;
if (right <= mid)
return query(left, right, root << 1);
else
if (left >= mid)
return query(left, right, root << 1| 1);
else{
return query(left, mid, root << 1) + query(mid + 1, right, root << 1 |1);
}
}
vector<int> countSmaller(vector<int>& nums) {
//前面还是跟树状数组一样,是离散化部分,只不过它需要判断nums为0的情况
//因为线段树需要自始至终left都小于right
vector<int> res = vector<int>(nums.size(), 0);
if(nums.size()==0)
return res;
vector<II> new_nums(nums.size());
vector<int> n(nums.size());
for (int i = 0; i<nums.size(); i++){
new_nums[i] = II(nums[i], i);
}
sort(new_nums.begin(), new_nums.end());
for (int i = 0; i<nums.size(); i++)
n[new_nums[i].second] = i + 1;
//注意线段树一定要开刀4N的大小,否则会有数组越界的情况
tree = vector<segtree>(nums.size() << 2);
buildtree(1, 1, nums.size());
for (int i = n.size() - 1; i >= 0; i--){
res[i] = query(1, n[i], 1);
update(n[i], n[i], 1, 1);
}
return res;
}
逆序对问题变形
补充,今天早上LeetCode周周赛又有一道这样的变种题:493. Reverse Pairs链接还没出,等到时候再补上。这道题跟上道题类似,找出这个数组中
i<j
且nums[i]>2*nums[j]
的元素总个数。
先说一下我的思路,我是直接套用315题的解法,只不过需要分成两个步骤,先是去查找按照nums[i]<2*val
的判断条件去查找,还是按照上面的方法,左子树走的话,不变,往右子树走的话res += LeftNum + 1
。找完之后在进行插入操作,插入就再按照nums[i]<val
的情况进行插入操作即可。
另外这题有一个坑点的地方在于会有INT_MAX的数出现,所以需要用到long long int
的类型,防止其乘二之后溢出。
然后看了第一名的大神用了个amazing的解法,即树状数组。大神方法如下,瞻仰一下:
class Solution {
public:
map<long long, int> bit;
long long lowbit(long long x)
{
return x & (-x);
}
void add(long long x)
{
//加34是为了处理负数情况
for(x += (1ll << 34); x <= (1ll << 36); x += lowbit(x))
++bit[x];
}
int que(long long x)
{
int ans = 0;
for(x += (1ll << 34); x; x -= lowbit(x))
ans += bit[x];
return ans;
}
int reversePairs(vector<int>& nums) {
bit.clear();
int n = nums.size();
int ans = 0;
for(int i = n - 1; i >= 0; --i)
{
ans += que((long long) nums[i] - 1ll);
add((long long) nums[i] * 2ll);
}
return ans;
}
};
看北理那个课件,也出现了一个poj上的题,有n个坐标点 (x,y) 让你求出每个坐标点左下角的元素个数。
这道题呢,表面上看上去与求逆序对无关不过需要再仔细想想,这个不就相当于一个二维尺度上的求找出小于一个数的全部数字吗。
先将所有 左边按照y值大小从小到大排序,然后按照x来找逆序对。