寻找给定区间内的第k小(大)的元…

来源:互联网 发布:2016三毛淘宝小号网址 编辑:程序博客网 时间:2024/06/02 10:19

第一节、寻找给定区间内的第k小(大)的元素

给定数组,给定区间,求第K小的数如何处理?常规方法请查阅:程序员编程艺术:第三章、寻找最小的k个数。

1、排序,快速排序。我们知道,快速排序平均所费时间为n*logn,从小到大排序这n个数,然后再遍历序列中后k个元素输出,即可,总的时间复杂度为O(n*logn+k)=O(n*logn)。

2、排序,选择排序。用选择或交换排序,即遍历n个数,先把最先遍历到得k个数存入大小为k的数组之中,对这k个数,利用选择或交换排序,找到k个数中的最小数kmax(kmax设为k个元素的数组中最小元素),用时O(k)(你应该知道,插入或选择排序查找操作需要O(k)的时间),后再继续遍历后n-k个数,x与kmax比较:如果x<kmax,则x代替kmax,并再次重新找出k个元素的数组中最大元素kmax‘(多谢jiyeyuran提醒修正);如果x<kmax,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),整趟下来,总的时间复杂度平均下来为:n*O(k)=O(n*k)。

3、维护k个元素的最大堆,原理与上述第2个方案一致,即用容量为k的最小堆存储最先遍历到的k个数,并假设它们即是最小的k个数,建堆费时O(k),有k1<k2<...kmax(kmax设为最大堆中的最小元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,若x<kmax,则更新堆(用时logk),否则不更新堆。这样下来,总费时O(k+(n-k)*logk)=O(N*logK)。此方法得益于在堆中,查找等各项操作时间复杂度均为logk(不然,就如上述思路2所述:直接用数组也可以找出最大的k个元素,用时O(n*k))。

4、按编程之美上解法二的所述,类似快速排序的划分方法,N个数存储在数组S中,再从数组中随机选取一个数X,把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有的元素+Sb中较小的k-|Sa|个元素。不断递归下去,把问题分解成更小的问题,平均时间复杂度为O(N)(编程之美所述的n*logk的复杂度有误,应为O(N),特此订正。其严格证明,请参考第三章:程序员面试题狂想曲:第三章、寻找最小的k个数、updated10次)......。


下面我们给出伴随数组解法,首先,定义一个结构体,一个是数组元素,另一个是数组原来的标号,记录每个数在数组的原顺序。

我们以下面的测试数据举例(红体部分表示下标为2~5之间的数5,2,6,3,蓝色部分表示给定区间中的数各自对应的数组下标,注,这里,我们让数组下标从1开始):

a[i].data 1 5 2 6 3 74
a[i].num  1 2 3 4 5 67

现在,题目要求:在原序列中下标2~5(即下标为2、3、4、5)之间找到第3小的数。问题亦相当于要你找原序列里给定区间即第2个数到第5个数之中(52 6 3)第3小的数(当然,答案很明显,第3小的数就是5)。

那么对原数组进行排序,然后得到的序列应该是(原下标保持不变):

a [i].data 1 2 3 45 6 7
a [i].num  1 35 7 24 6

如上,既然数据现在已经从小到大排好了,那么,我们只需要进行一次检索,从最小的数到最大的数,我们找第k(k=3)小的数,当我们发现下标a[i].num等于原下标索引为2~5中的任一数值,亦即a[i].data==5|| 2 || 6 ||3的时候,k--,那么当k==0的时候,我们也就找到了第k(3)小的数了。如下(红色部分表示原给定区间中的数,浅色部分依然是原各数对应的下标):

a [i].data 1 2 3 45 6 7
a [i].num  1 35 7 24 6
       3 2 1 1 0

故下标索引为2~5之间第k(3)小的数是5。

程序的构造与解释:由于排序后,我们能保证原序列已经从小到大的排好序了,所以,当遍历或扫描到原序列给定区间中的数时,则k--,最终能在k==0时,找到第k小的数,且这个数是在原来给定区间中的某一个数。
而这个伴随数组,或者说原序列各数的索引则帮我们或者说是帮电脑记下了原来的数,已让我们后来遍历时能识别排序后序列中的数是否是给定区间中的某一个数。如果是原给定区间中的数,则k--,否则k不变。


第二节、采用伴随数组方案的实现
上述采用伴随数组的方法巧妙且简单,也很好理解和实现,关键就是在于题目要求是在给定区间的区间找寻第k小(大)的元素,所以,基本上在排序n*logn完了之后,总能在O(n)的时间内找到想找的数。源代码如下:

view plaincopy to clipboardprint?
//copyright@ 水 && July
//总的时间复杂度为O(N*logN+N)=O(N*logN)。

#include<iostream>
#include<algorithm>
using namespace std;

struct node{
int num,data;
bool operator < (const node &p)const
{
return
data < p.data;
}
};
node p[100001];

int main()
{
int n=7;
int i,j,a,b,c;//c:flag;

for(i=1;i<=n;i++)
{
scanf("%d",&p[i].data);
p[i].num = i;
}
sort(p+1,p+1+n); //调用库函数sort完成排序,复杂度n*logn

scanf("%d %d%d",&a,&b,&c);
for(i=1;i<=n;i++) //扫描一遍,复杂度n
{
if(p[i].num>=a &&p[i].num<=b)
c--;
if(c == 0)
break;
}
printf("%d\n",p[i].data);
return 0;
}

 

编程独白:

给你40分钟的时间,你可以思考十分钟,然后用三十分钟的时间来写代码,最后浪费在无谓的调试上;你也可以思考半个小时,彻底弄清问题的本质与程序的脉络,然后用十分钟的时间来编写代码,体会代码如行云流水而出的感觉。

本章总结:

伴随数组这种方式确实比较新颖 ,伴随数组的前提是在排序后的 ,但总的复杂度还是0(N*logN+N)=O(N*logN),找第K大的数的此类面试题都是有这几点限制:1、数很多,让你在内存中放不下,2、复杂度严格要求,即不能用排序。

对于这个伴随数组,我们在解决 “给定区间的区间找寻第k小(大)的元素” 这个问题,还是选择堆为好,在之前的基础上:入堆的时候只需检测这个元素的下标是否是给定区间内的,不是则不入这样的复杂度会低,不需要排序。(非常感谢雨翔的意见)。

0 0
原创粉丝点击