查找最小的K个元素

好吧,看到这道题的时候,我无节操的笑了。记着这学期刚开学的时候,小速问过我这道题,我就借这个机会,把当时所有的想法写出来。另外,csdn的july也曾经对此题写过巨幅文字,推荐大家欣赏k

这道题也是实在太经典,google,microsoft等各大公司都曾经把它作为面试题,这道题的难度系数还是相对高一些的。

首先,我们看题目描述:

题目:输入n个整数,输出其中最小的k个。 
例如输入1,2,3,4,5,6,7和8这8个数字,则最小的4个数字为1,2,3和4。 


逻辑分析:

1、题目没有对时间复杂度,空间复杂度的要求,发挥空间是比较大的。小速把题目丢给我的时候,我的第一反应就是:太简单了,用数组读入数据,直接快速排序,然后输出前k个。时间复杂度O(nlgn+k)=O(nlgn),快速排序是原地排序,不需要额外空间复杂度,那么,空间上占用是常数O(1)(n个已知元素,占用空间复杂度是O(1))。


2、在做了上面的回复之后,我马上意识到自己无脑了,我们没有必要对数组完全排序,只要能挑出前k个最小的就好,那么,自然而然就会想到选择排序或者交换排序,即遍历n个数,先把先遍历到的k个数存入数组,然后找出最大的数kmax,你应该知道,对于交换排序或者选择排序,查找操作时间复杂度是o(k),接着对比第k+1个数与kmax,如果小于kmax,则用a[k+1]替换kmax,重新找出kmax',然后继续k+2,直到n。这样,时间复杂度是n*O(k)=O(n*k)。


3、继而就想到了堆,最大堆,用容量为k的最大堆存储最先遍历的k个数,建堆O(k),调整堆O(lgk)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,如果小于,则替换,重新调整堆,总耗时O(k*logk+(n-k)*logk)=O(n*logk),此方法得益于在堆中,查找等各项操作时间复杂度均为logk(不然,就如上述思路2所述:直接用数组也可以找出最大的k个元素,用时O(n*k))。


4、在给出上面的作答后,蓦地想到了算法导论上,貌似是第七章的最后一节,对这个问题做过分析,利用的是一种很复杂的线性时间解法,即O(n)。没错,仔细分析后,这题是可以用线性时间复杂度完成的。

而《编程之美》上也有这道题的线性时间解法,类似快速排序的划分方法,N个数存储在数组S中,再从数组中随机选取一个数X,把数组划分为Sa和Sb俩部分,Sa>=X>=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较大的k个元素,否则返回Sa中所有的元素+Sb中最大的k-|Sa|个元素。不断递归下去,把问题分解成更小的问题,平均时间复杂度为O(N)。而随机选取的这个元素,实际上就是导致时间复杂度的优劣。想要保证此快速选择算法为O(N)的复杂度,只有两种途径,那就是保证划分的枢纽元元素的选取是:
1、随机的(注,此枢纽元随机不等同于数组随机)
2、五分化中项的中项,或中位数的中位数。

关于上面的论述,july曾与飞羽有过激烈的争讨,有兴趣的可以去看看相应文章,链接:

http://blog.csdn.net/v_JULY_v/article/details/6403777


5、或许第4种思路,对于刚刚接触算法的道友,不是很好理解,那么,我在这里通俗的谈谈,其实,上述算法实质上是减少元素间比较次数来优化时间复杂度的,我们知道,基于比较排序的算法,时间复杂度不会优于O(nlgn),通过画一棵决策树,可以清楚的证明。这里不再赘述,相关内容请参考《算法导论》相关章节。而基于快速排序的模型,我们每一次随机选取的元素做为主元分界,只需要根据k的大小,处理主元的单一一侧,进行递归,显然,决策树每一层我们都丢掉一个分支,显然,决策树退化成了链,但是,主元的选择很关键,如果选择上不满足随机,或者像上述的五分化中项的中项,那么决策树一开始就退化成最差的链表,每一次的取舍都相当于无用功。

算法导论上提出的无分化中项的中项,也就是中位数的中位数,是很有道理的。道理在于,每一次选取的这个主元,会淘汰掉当前规模的1/4,因为至少有1/4一定比主元大,也至少有1/4比主元小,那么,只需要根据k,来取舍就行,每次规模减少1/4,递归下去,由主方法可得解。


玉涵注:上述对算法分析的描述,有困难的朋友可以跳过,我虽然不提倡,但也不反对用直觉来思考。


下面贴出3种算法的实现,感谢july,飞羽等大神的引路。

///下面的代码对July博客中的三个版本代码进行重新改写。欢迎指出错误。    
///先把它们贴在这里,还要进行随机化数据测试。待发...    
    
//modified by 飞羽 at 2011.5.11    
/Top_K_test    
    
//修改了下命名规范,July、updated,2011.05.12。    
#include <iostream>    
#include <stdlib.h>    
using namespace std;    
    
inline int my_rand(int low, int high)    
{    
    int size = high - low + 1;    
    return  low + rand() % size;    
}    
    
int partition(int array[], int left, int right)    
{    
    int pivot = array[right];    
    int pos = left-1;    
    for(int index = left; index < right; index++)    
    {    
        if(array[index] <= pivot)    
            swap(array[++pos], array[index]);    
    }    
    swap(array[++pos], array[right]);    
    return pos;//返回pivot所在位置    
}    
    
bool median_select(int array[], int left, int right, int k)    
{    
    //第k小元素,实际上应该在数组中下标为k-1    
    if (k-1 > right || k-1 < left)       
        return false;    
    
    //真正的三数中值作为枢纽元方法,关键代码就是下述六行    
    int midIndex=(left+right)/2;    
    if(array[left]<array[midIndex])    
        swap(array[left],array[midIndex]);    
    if(array[right]<array[midIndex])    
        swap(array[right],array[midIndex]);    
    if(array[right]<array[left])    
        swap(array[right],array[left]);    
    swap(array[left], array[right]);    
        
    int pos = partition(array, left, right);    
        
    if (pos == k-1)    
        return true;    
    else if (pos > k-1)    
        return median_select(array, left, pos-1, k);    
    else return median_select(array, pos+1, right, k);    
}    
    
bool rand_select(int array[], int left, int right, int k)    
{    
    //第k小元素,实际上应该在数组中下标为k-1    
    if (k-1 > right || k-1 < left)       
        return false;    
    
    //随机从数组中选取枢纽元元素    
    int Index = my_rand(left, right);    
    swap(array[Index], array[right]);    
        
    int pos = partition(array, left, right);    
        
    if (pos == k-1)    
        return true;    
    else if (pos > k-1)    
        return rand_select(array, left, pos-1, k);    
    else return rand_select(array, pos+1, right, k);    
}    
    
bool kth_select(int array[], int left, int right, int k)    
{    
    //直接取最原始的划分操作    
    if (k-1 > right || k-1 < left)       
        return false;    
    
    int pos = partition(array, left, right);    
    if(pos == k-1)    
        return true;    
    else if(pos > k-1)    
        return kth_select(array, left, pos-1, k);    
    else return kth_select(array, pos+1, right, k);    
}    
    
int main()    
{    
    int array1[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};     
    int array2[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};     
    int array3[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};     
        
    int numOfArray = sizeof(array1) / sizeof(int);    
    for(int i=0; i<numOfArray; i++)    
        printf("%d/t",array1[i]);    
        
    int K = 9;    
    bool flag1 = median_select(array1, 0, numOfArray-1, K);    
    bool flag2 = rand_select(array2, 0, numOfArray-1, K);    
    bool flag3 = kth_select(array3, 0, numOfArray-1, K);    
    if(!flag1)     
        return 1;    
    for(i=0; i<K; i++)    
        printf("%d/t",array1[i]);    
    printf("/n");    
        
    if(!flag2)     
        return 1;    
    for(i=0; i<K; i++)    
        printf("%d/t",array2[i]);    
    printf("/n");    
        
    if(!flag3)     
        return 1;    
    for(i=0; i<K; i++)    
        printf("%d/t",array3[i]);    
    printf("/n");    
        
    return 0;    
}  



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您好,关于Python实现查找最小的k个数,我可以提供以下两种常见的方法: 方法一:使用堆 将数组前k个元素建立小根堆,然后遍历剩余元素,若比堆顶元素小,则将其加入堆中,同时将堆顶元素删除,最终堆中的k个元素即为所求。具体代码如下: import heapq def find_k_min(nums, k): if not nums or k <= 0 or k > len(nums): return None res = [] for i in range(k): heapq.heappush(res, nums[i]) for i in range(k, len(nums)): if nums[i] < res[0]: heapq.heappop(res) heapq.heappush(res, nums[i]) return res 方法二:使用快排的思想 使用快排的思想,每次选择一个基准元素,将小于基准元素的放在左边,大于等于基准元素的放在右边,然后判断基准元素的位置,若该位置等于k-1,则基准元素以及左边的元素就是所求;若该位置小于k-1,则在右半边继续查找;若该位置大于k-1,则在左半边继续查找。具体代码如下: def find_k_min(nums, k): if not nums or k <= 0 or k > len(nums): return None left, right = 0, len(nums)-1 while True: index = partition(nums, left, right) if index == k-1: return nums[:k] elif index < k-1: left = index + 1 else: right = index - 1 def partition(nums, left, right): pivot = nums[left] while left < right: while left < right and nums[right] >= pivot: right -= 1 nums[left] = nums[right] while left < right and nums[left] < pivot: left += 1 nums[right] = nums[left] nums[left] = pivot return left 希望能对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值