问题描述:给定一个具有n个元素的实数集,一个实数t,一个整数k,如何快速的确定该实数集是否存在一个k个元素的子集,
其中各个元素的的总和之多为t
。
拿到这个题目,我首先想到的是快排,但是使用快排算法对n个元素进行排序,时间复杂度为nlog(n)。如果你使用的是堆排序的话,
那么效率又提高了,因为在这个题目中,我们需要的只是前k个最小元素,所以使用堆排序更合理,时间复杂度为nlog(k),但是作者在书中给出的提示
是时间复杂度需要达到o(n)级别,所以上述两种方法全部失效了。
其实这是一个典型的topk问题,就是在一个集合中,找出前k个最小或者最大的数。问题的关键在于找出第k小个数。这里我参考别人的思想:
本算法跟快排的思想相似,首先在数组中选取一个数centre作为枢纽,将比centre小的数,放到centre的前面将比centre大的数,放到centre的后面。如果此时centre的位置刚好为k,则centre为第k个最小的数;如果此时centre的位置比k前,则第k个最小数一定在centre后面,递归地在其右边寻找;如果此时centre的位置比k后,则第k个最小数一定在centre后面,递归地在其左边寻找。
注意:centre的位置=其下标值+1,因为数组中的第一个元素的下标为0。
其中各个元素的的总和之多为t
。
拿到这个题目,我首先想到的是快排,但是使用快排算法对n个元素进行排序,时间复杂度为nlog(n)。如果你使用的是堆排序的话,
那么效率又提高了,因为在这个题目中,我们需要的只是前k个最小元素,所以使用堆排序更合理,时间复杂度为nlog(k),但是作者在书中给出的提示
是时间复杂度需要达到o(n)级别,所以上述两种方法全部失效了。
其实这是一个典型的topk问题,就是在一个集合中,找出前k个最小或者最大的数。问题的关键在于找出第k小个数。这里我参考别人的思想:
本算法跟快排的思想相似,首先在数组中选取一个数centre作为枢纽,将比centre小的数,放到centre的前面将比centre大的数,放到centre的后面。如果此时centre的位置刚好为k,则centre为第k个最小的数;如果此时centre的位置比k前,则第k个最小数一定在centre后面,递归地在其右边寻找;如果此时centre的位置比k后,则第k个最小数一定在centre后面,递归地在其左边寻找。
注意:centre的位置=其下标值+1,因为数组中的第一个元素的下标为0。
从上面的描述中,我们可以看到这个算法运用了减治的方法求解。减治的思想与分治非常相似,同样是在一次操作中,削减问题的规模,只是分治把每个子问题求解后,要合并每个子问题的解才能得到问题,而减治的方法,却不用合并子问题的解,子问题的解,直接就是原问题的解。举个例子来说,就像快排和二分查找算法,前者是分治,后者是减治。因为快排要等到所有的子数组都排完序,原数组才有序,而二分查找却不用,它每执行一次查找,直接丢弃一半的数组,而不用合并子问题的解。不过也有不少书,把他们都归为分治法。
代码如下:
//问题描述:给定一个具有n个元素的实数集,一个实数t,一个整数k,如何快速的确定该实数集是否存在一个k个元素的子集,其中各个元素的的总和之多为t
// 程序的时间复杂度为o(n)。
// 首先需要在数组中找出第k个小的数字。
#include<iostream>
#include <algorithm>
using namespace std;
int find_kth_min(int *array, int left,int right,const int k,const int size )
{
if (left >= right || left < 0 || k < 1 || k > right + 1){ //注意考虑极端情况。
cerr << "接受的参数有误" << endl;
exit(1);
}
int center = array[right];
int i = left;
int j = right - 1;
while(true){ //经过这一步,所有的比center大的都在center的右边,所有的比center小的都在center的左边
while(array[i] <= center && i < size-1) //不能让数组越界!否则就等着悲剧把
++ i;
while(array[j] >= center && j >= 1) //不能让数组越界!否则就等着悲剧吧
-- j;
if (i < j)
swap(array[i],array[j]);
else break;
}
swap(array[i],array[right]); //以arrar[right]为分界线,左边全是比他小的,右边全是比他大的
if (i+1 == k){
return array[i];
}
else if (i+1 < k){
find_kth_min(array,i+1,right,k,size);
}
else {
find_kth_min(array,left,i-1,k,size);
}
}
int main()
{
const int k = 6; //返回第六小的数,注意从1开始计数。
int a[] = {2,4,6,7,5,11,3,5,6,32,5,2,4};
const int size = sizeof(a)/sizeof(int); //这种方式计算数组的长度,应该鼓励使用。
int k_th_min;
k_th_min = find_kth_min(a,0,size-1,k, size); //返回第k小的数
//下面计算前k个小的数之和是不是小于t。
int sum = 0; //sum是计算前k个小的数字的总和
int number = 0;
for(int i = 0;i < size; ++i){
if (a[i] < k_th_min){
sum += a[i];
number ++;
}
}
number ++;
if (number < k){ //这里要特别注意了,表明第K个最小的数,不止一个,那么就需要修正上面计算的sum
sum += (k-number)*k_th_min;
number += k-number;
}
sum += k_th_min;
cout << number << " " << sum; //得到最终的前k个最小的数的总和.
return 0;
}
这个算法的时间复杂度,平均来说是o(n),解释来自
王晓东的计算机算法分析与设计中的第二章第9节,即2.9中有这个算法的描述和介绍。里面说它的时间复杂度为O(N),好像计算这个时间复杂度需要用到微分,我没看懂,结论就是这个算法的平均时间复杂度是o(n).惭愧,需要学习的东西很多。如果有问题,欢迎指正,谢谢,我的QQ1527927373.
另外程序参考了ljianhui的专栏。但是它的程序中出现了一些小错误,已经被我改正。这个算法的好处是在求topk时,平均的时间复杂度是o(N)