如何快速求出(在log2n的时间复杂度内)整数区间[x,y]中第k大的值(x<=k<=y)?
其实我刚开始想的是用快排来查找,但是其实这样是不行的,因为会破坏原序列,就算另外一个数组来存储,多次询问的条件下,同样满足不了。
划分树主要是针对上述问题而设计的。 划分树是一种基于线段树的数据结构,其基本思想就是将待查找的区间分为两个子区间:不大于数列中间 值的元素被分配到左儿子的子区间,简称左子区间;大于数列中间值的元素被分配到右儿子的子区间,简称右子区间。
显然,左子区间的数小于右子区间的数。建树 的时候要注意,对于被分到同一子树的元素,元素间的相对位置不能改变。例如构建整数序列[1 5 2 3 6 4 7 3 0 0]的划分树:
整数序列经排序后得到[0 0 1 2 3 3 4 5 6 7],中间值是3。划分出下一层的左子区间[1 2 3 0 0],中间值为1;下一层的由子区间[5 6 4 7 3],中间值为5;以中间值为界,划分出当前层每个区间的下层左右子区间······以此类推,直至划分出所有子区间含单个整数为止。
这里可能有人就会提问了,为什么两个子区间是这样的呢?? 其实我刚开始也这么想,下面给出解释:
1.如果有和中间值相等的元素,插入进去的前提是 小于中间值的元素不够填满一个区间,差多少就补多少和中间值相等的元素(很好理解,小于肯定优先嘛)。
2.相对位置不能改变。
下面给出一个图,还是以[1 5 2 3 6 4 7 3 0 0]为例:
如图可见,划分树是一颗完全二叉树,树叶为单元素区间。若数列含有n个整数,则对应的划分树有[log 2 n]+1层。
查找的时候通过记录进入左子树的数的个数,确定下一个查找区间,直至查找范围缩小到单元素为止。 此时,区间内的第K大值就找到了。
整个算法分两步:
1.建树-------离线构建整个查询区间的划分树
2.查找-------在划分树中查找某个子区间中的第K大值。
在查询之前,我们先离线构建整个查询区间的划分树。 建树过程比较简单,对于区间[l,r],首先通过对原数列排序找到这个区间的中间值的位置mid(mid=[(l+r)/2]),不大于中间值的数划入左子树[l,mid], 大于中间值的数划入右子树[mid+1,r] 。同时,对于第i 个数,记录在[l,i] 区间内有多少整数被划入左子树。 最后继续对它的左子树[l,mid] 和右子树[mid+1,r] 递归建树,直至划分出最后一层的叶节点为止,显然,建树过程是自上而下的。
具体实现办法:
先将读入的数据排序成sorted[] (从小到大排序) ,取区间[l,r]的中间值sorted[mid],然后遍历[l,r]区间,依次将每个整数划分到左子区间或者右子区间中去。 注意,被分到每个子树的整数是相对有序的。 注意,这里要记录区间能插入多少个和中间值相等的元素。 另外,在这个过程中,要记录一个类似前缀和的东西,即 l 到 i 这个区间内有多少整数被划分到左子树。设:
tree [p][i] ------第p层第i个位置的整数值,初始序列为tree[0][]。
sorted[]---------排序序列,即存储tree[0][]排序后的结果
toleftp[][]--------toleft[p][i],表示第p层前i个数中有多少个整数分入下一层的左子区间
lpos和rpos-----下一层左子区间和右子区间的指针
same------------当前区间等于中间值的个数
下面给出建树的具体代码:
void Build(int l,int r,int dep)//左边界 右边界 第几层 { if(l==r) return ; int mid=(l+r)>>1; int same=(mid-l+1);//左子树可以有多少个等于mid的数 for(int i=l;i<=r;i++) { if(tree[dep][i]<sorted[mid]) same--;//计算可以放多少个等于mid值的数,保证相对位置不变 } int lpos=l; int rpos=mid+1; for(int i=l;i<=r;i++) { if(tree[dep][i]<sorted[mid]) { tree[dep+1][lpos++]=tree[dep][i]; } else if(tree[dep][i]==sorted[mid]&&same>0) { tree[dep+1][lpos++]=tree[dep][i]; same--; } else { tree[dep+1][rpos++]=tree[dep][i]; } toleft[dep][i]=toleft[dep][l-1]+lpos-l; } Build(l,mid,dep+1); Build(mid+1,r,dep+1); }
在划分树上查询子区间[l,r]中第K大的数呢?(L<=l,r<=R)? 我们从划分树根出发,自上而下查找:
若查询至叶子(l==r),则该整数(tree[dep][l]) 为子区间[l,r]中第K大的数;否则区间[L,l-1]内有toleft[dep][l-1]个整数进入下一层的左子树,区间[L,r]内有toleft[dep][r]个整数进入下一层的左子树。显然,在查询子区间[l,r]中有cnt=toleft[dep][r]-toleft[dep][l-1]个整数进入到下一层的左子树。如果cnt>=k,则递归查询左子树(对应区间[L,mid]);否则递归查询右子树(对应区间[mid+1,R).。
难点是,如何在对应子树的大区间中计算被查询的子区间[newl,newr]?
若递归查询左子树,则当前子区间的左边是上一层[L,l-1]里的toleft[dep][l-1]-toleft[dep][L-1]个整数,由此可得到newl=L+toleft[dep][l-1]-toleft[dep][L-1]; 加上上一层查询区间[l,r]中cnt个整数,newr=newl+cnt-1(这里-1是为了处理边界问题,值得大家认真思索),即在大区间[L,mid]里查询子区间[newl,newr]中第K大值。(这里看文字有点抽象,最好看上面的图来理解)
同理,若递归查询右子树,则[l,r]中有r-l-cnt个整数进入右子树,需要计算其中第K-cnt大的整数值。关键是如何计算被查询子区间的右指针newr:
当上一层的[l,r]和[r+1,R]中有若干个整数被分配至下一层左子区间,其中[r+1,R]中有toleft[dep][R]-toleft[dep][r]个整数位于左子区间的右方,因此查询子区间的右指针移至r右方的第toleft[dep][R]-toleft[dep][r] 个位置,即newr=r+toleft[dep][R]-toleft[dep][r]。显然,左指针newl=newr-(r-l-cnt)。
下面给出具体实现的代码:
int query(int L,int R,int l,int r,int dep,int k) { if(l==r) return tree[dep][l]; int mid=(L+R)>>1; int cnt=toleft[dep][r]-toleft[dep][l-1]; if(cnt>=k) { int newl=L+toleft[dep][l-1]-toleft[dep][L-1]; int newr=newl+cnt-1; return query(L,mid,newl,newr,dep+1,k); } else { int newr=r+toleft[dep][R]-toleft[dep][r];//相当于把r挤到这个位置 int newl=newr-(r-l-cnt); return query(mid+1,R,newl,newr,dep+1,kcnt); } }