编程珠玑第二章第八题的解答

来源:互联网 发布:矩阵范数 编辑:程序博客网 时间:2024/06/05 07:36
      问题描述:给定一个具有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。

     从上面的描述中,我们可以看到这个算法运用了减治的方法求解。减治的思想与分治非常相似,同样是在一次操作中,削减问题的规模,只是分治把每个子问题求解后,要合并每个子问题的解才能得到问题,而减治的方法,却不用合并子问题的解,子问题的解,直接就是原问题的解。举个例子来说,就像快排和二分查找算法,前者是分治,后者是减治。因为快排要等到所有的子数组都排完序,原数组才有序,而二分查找却不用,它每执行一次查找,直接丢弃一半的数组,而不用合并子问题的解。不过也有不少书,把他们都归为分治法。

        代码如下:

//问题描述:给定一个具有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个最小的数,不止一个,那么就需要修正上面计算的sumsum += (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)

0 0