题目
描述
给定一个整数数组 (下标由 0 到 n-1,其中 n 表示数组的规模,数值范围由 0 到 10000),以及一个 查询列表。对于每一个查询,将会给你一个整数,请你返回该数组中小于给定整数的元素的数量。
样例
样例 1:
输入: array =[1,2,7,8,5] queries =[1,8,5]
输出:[0,4,2]
样例 2:
输入: array =[3,4,5,8] queries =[2,4]
输出:[0,1]
分析
利用这道题来练习一下线段树,我首先用的是区间和线段树超时了,然后用了计数线段树是可以过的,我们先来分析一下这两种方法的时间复杂度
给定数组长度n 查询数组长度m
1.区间和线段树
构造线段树
构造线段树按照叶子节点的数量即可,时间复杂度O(n)
查询
查询的次数为m 每次查询消耗为线段树的高度h,而线段树的高度又是由给定数组长度n决定的,这里为logn
所以总的时间复杂度为O(mlogn)
修改
这里不需要修改
2.计数线段树
构造线段树
这里每个节点代表这个区间数字的个数 比如[0,1]代表0的个数+1的个数
这里最大值为10000
所以我们可以认为时间复杂度为O(10000)
查询
同样跟线段树的高度有关,跟上面的同理,这里为O(log10000)
查询次数为m
总时间复杂度O(mlog10000)
修改
这里修改的次数为给定数组长度n,每次修改消耗的时间为树的高度,同理为log10000
总的时间复杂度O(nlog10000)
比较一下可以发现如果n非常大,区间和线段树更占优势,log10000大概为10几,而区间和线段树它的节点数量为2n-1,常数项为2,大概会比计数线段树快6,7倍的样子
但是当m和n都很大,都接近1e8时,计数线段树常数项都是log1e4,区间和线段树常数项为log1e8,大概是计数的两倍多,而且构造也会花费n的时间,所以这里用计数线段树更合适
这道题没有给出n和m的范围,就当作练习了,区间和之前发过了,这里就发一个计数线段树
代码部分
1.线段树节点
//线段树节点
class SegmentTreeNode
{
public:
int start,end;
int count;
SegmentTreeNode* left;
SegmentTreeNode* right;
SegmentTreeNode(int s,int e,int c):start(s),end(e),count(c)
{
left=NULL;
right=NULL;
}
};
2.线段树的构造
这里我们把所有节点都计数都设置为0,在修改的部分,在把给定的数组中的每个数字进行计数
SegmentTreeNode* build(int start,int end)
{
//出口
if(start==end)
{
return new SegmentTreeNode(start,start,0);
}
//现在能做的事情
SegmentTreeNode* _root=new SegmentTreeNode(start,end,0);
int mid=(start+end)/2;
_root->left=build(start,mid);
_root->right=build(mid+1,end);
_root->count=_root->left->count+_root->right->count;
return _root;
}
3.线段树的查询
跟之前的查询一样,节点的计数等于它左区间计数加右区间计数
int query(SegmentTreeNode* _root,
int start,int end,int qstart,int qend)
{
//出口
if(start>=qstart&&end<=qend)
{
return _root->count;
}
if(end<qstart||start>qend)
{
return 0;
}
//现在能做的事情
int mid=(start+end)/2;
int leftval=query(_root->left,start,mid,qstart,qend);
int rightval=query(_root->right,mid+1,end,qstart,qend);
return leftval+rightval;
}
4.线段树的修改
这里是计数线段树的重点,递归到叶子节点后,我们要用当前节点的计数加上一,因为给定数组中可能出现重复的数字,其他没有变化
void modify(SegmentTreeNode* _root,
int start,int end,int index,int value)
{
//出口
if(start==end&&start==index)
{
_root->count+=value;
return;
}
//现在能做的事情
int mid=(start+end)/2;
if(index<=mid)
{
modify(_root->left,start,mid,index,value);
}
else
{
modify(_root->right,mid+1,end,index,value);
}
_root->count=_root->left->count+_root->right->count;
}
完整代码
#include <bits/stdc++.h>
using namespace std;
//线段树节点
class SegmentTreeNode
{
public:
int start,end;
int count;
SegmentTreeNode* left;
SegmentTreeNode* right;
SegmentTreeNode(int s,int e,int c):start(s),end(e),count(c)
{
left=NULL;
right=NULL;
}
};
//线段树
class SegmentTree
{
public:
SegmentTreeNode* root;
SegmentTree(int len)
{
root=build(0,len);
}
SegmentTreeNode* build(int start,int end)
{
//出口
if(start==end)
{
return new SegmentTreeNode(start,start,0);
}
//现在能做的事情
SegmentTreeNode* _root=new SegmentTreeNode(start,end,0);
int mid=(start+end)/2;
_root->left=build(start,mid);
_root->right=build(mid+1,end);
_root->count=_root->left->count+_root->right->count;
return _root;
}
int query(SegmentTreeNode* _root,
int start,int end,int qstart,int qend)
{
//出口
if(start>=qstart&&end<=qend)
{
return _root->count;
}
if(end<qstart||start>qend)
{
return 0;
}
//现在能做的事情
int mid=(start+end)/2;
int leftval=query(_root->left,start,mid,qstart,qend);
int rightval=query(_root->right,mid+1,end,qstart,qend);
return leftval+rightval;
}
void modify(SegmentTreeNode* _root,
int start,int end,int index,int value)
{
//出口
if(start==end&&start==index)
{
_root->count+=value;
return;
}
//现在能做的事情
int mid=(start+end)/2;
if(index<=mid)
{
modify(_root->left,start,mid,index,value);
}
else
{
modify(_root->right,mid+1,end,index,value);
}
_root->count=_root->left->count+_root->right->count;
}
};
class Solution {
public:
vector<int> countOfSmallerNumber(vector<int> &a, vector<int> &queries) {
vector<int> ans;
SegmentTree st(10001);
for(int i=0;i<a.size();i++)
{
st.modify(st.root,0,10000,a[i],1);
}
for(int i=0;i<queries.size();i++)
{
int cnt=st.query(st.root,0,10000,0,queries[i]-1);
ans.push_back(cnt);
}
return ans;
}
};
int main (void)
{
vector<int> a={1,2,7,8,5};
vector<int> q={1,8,5};
vector<int> ans;
Solution s;
ans=s.countOfSmallerNumber(a,q);
for(int i=0;i<ans.size();i++)
cout<<ans[i]<<" ";
return 0;
}
总结
熟悉了一种模板之后,我们应该多熟悉这些模板的使用以及各种变形,然后根据题目给出的数据范围,时间限制,内存限制,来决定用哪种模板,用这种模板的利弊,以及怎么样变形