题目描述:输入n个整数,输出其中最小的k个元素。

例如:输入1,2,3,4,5,6,7,8这8个数字,则最小的4个数字为1,2,3,4。

思路1:最容易想到的方法:先对这个序列从小到大排序,然后输出前面的最小的k个数即可。如果选择快速排序法来进行排序,则时间复杂度:O(n*logn)

思路2:在思路1的基础上更进一步想想,题目并没有要求要查找的k个数,甚至后n-k个数是有序的,既然如此, 咱们又何必对所有的n个数都进行排序列?如此,我们能想打的一个方法是:遍历n个数,先把最先遍历到得k个数存入大小为k的数组之中,对这k个数,利用选 择或交换排序,找到k个数中的最大数kmax(kmax设为k个元素的数组中最大元素),用时O(k)(你应该知道,插入或选择排序查找操作需要O(k) 的时间),后再继续遍历后n-k个数,x与kmax比较:如果x<kmax,则x代替kmax,并再次重新找出k个元素的数组中最大元素kmax ‘;如果x>kmax,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),整趟下来,总的时间复杂度平均下来 为:n*O(k)=O(n*k)

思路3:与思路2方法类似,只是用容量为k的最大堆取代思路2中数组的作用(从数组中找最大数需要O(k)次查 找,而从更新一个堆使之成为最大堆只需要O(logk)次操作)。具体做法如下:用容量为k的最大堆存储最先遍历到的k个数,并假设它们即是最小的k个 数,建堆费时O(k)后,有k1<k2<…<kmax(kmax设为大顶堆中最大元素)。继续遍历数列,每次遍历一个元素x,与堆顶元 素比较,x<kmax,更新堆(用时logk),否则不更新堆。这样下来,总费时O(k+(n-k)*logk)=O(n*logk)。

思路4:按编程之美中给出的描述,类似快速排序的划分方法,N个数存储在数组S中,再从数组中随机选取一个数X(随机选取枢纽元,可做到线性期望时间O(N)的复杂度),把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素。像上述过程一样,这个运用类似快速排序的partition的快速选择SELECT算法寻找最小的k个元素,在最坏情况下亦能做到O(N)的复杂度。oh,太酷了,有没有!

思路5:仍然用到数据结构:堆。具体做法为:针对整个数组序列建最小堆,建堆所用时间为O(n),然后取堆中的前k个数,总的时间复杂度即为:O(n+k*logn)。

思路6:与上述思路5类似,不同的是在对元素数组原地建最小堆O(n)后,然后提取K次,但是每次提取时,换到顶部的元素只需要下移顶多k次就足够了,下移次数逐次减少(而上述思路5每次提取都需要logn,所以提取k次,思路7需要k*logn。而本思路只需要K^2)。 此种方法的复杂度为O(n+k^2)。此方法可能不太直观,一个更直观的理解是:每次取出堆顶元素后,最小堆的性质被破坏了,我们需要调整最小堆使之满足 最小堆的性质。由于我们只需要求取前k个数,我们无需将整个堆都完整的调整好,只需保证堆的最上面k个数是最小的就可以,即第一趟调整保持第0层到第k层 是最小堆,第二趟调整保持第0层到第k-1层是最小堆…,依次类推。

在编码实现上述思路之前,你可能需要了解:快速排序堆排序

思路3的一个实现:

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#define PARENT(i) (i)/2
#define LEFT(i) 2*(i)+1
#define RIGHT(i) 2*(i+1)

void swap(int *a,int *b)
{
*a=*a^*b;
*b=*a^*b;
*a=*a^*b;
}
void max_heapify(int *arr,int index,int len)
{
int l=LEFT(index);
int r=RIGHT(index);
int largest;
if(l<len && arr[l]>arr[index])
largest=l;
else
largest=index;
if(r<len && arr[r]>arr[largest])
largest=r;
if(largest != index){//将最大元素提升,并递归
swap(&arr[largest],&arr[index]);
max_heapify(arr,largest,len);
}
}

void build_maxheap(int *arr,int len)
{
int i;
if(arr==NULL || len<=1)
return;
for(i=len/2+1;i>=0;--i)
max_heapify(arr,i,len);
}

void k_min(int *arr,int len,int k)
{
int i;
build_maxheap(arr,k);
for (i = k;i < len;i++)
{
if (arr[i] < arr[0])
{
arr[0] = arr[i];
max_heapify(arr,0,k);
}
}
}
/*
void heap_sort(int *arr,int len)
{
int i;
if(arr==NULL || len<=1)
return;
build_maxheap(arr,len);

for(i=len-1;i>=1;--i){
swap(&arr[0],&arr[i]);
max_heapify(arr,0,--len);
}
}
*/

int main()
{
int arr[10]={91,8,6,82,15,18,7,46,29,12};
int i;
k_min(arr,10,4);
for(i=0;i<10;++i)
printf("%d ",arr[i]);
system("pause");
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#define PARENT(i) (i)/2
#define LEFT(i) 2*(i)+1
#define RIGHT(i) 2*(i+1)
 
void swap(int *a,int *b)
{
    *a=*a^*b;  
    *b=*a^*b;  
    *a=*a^*b;  
}
void max_heapify(int *arr,int index,int len)
{
    int l=LEFT(index);
    int r=RIGHT(index);
    int largest;
    if(l<len && arr[l]>arr[index])
        largest=l;
    else
        largest=index;
    if(r<len && arr[r]>arr[largest])
        largest=r;
    if(largest != index){//将最大元素提升,并递归
        swap(&arr[largest],&arr[index]);
        max_heapify(arr,largest,len);
    }
}
 
void build_maxheap(int *arr,int len)
{
    int i;
    if(arr==NULL || len<=1)
        return;
    for(i=len/2+1;i>=0;--i)
        max_heapify(arr,i,len);
}
 
void k_min(int *arr,int len,int k)
{
    int i;
    build_maxheap(arr,k);
    for (i = k;i < len;i++)
    {
        if (arr[i] < arr[0])
        {
            arr[0] = arr[i];
            max_heapify(arr,0,k);
        }
    }
}
/*
void heap_sort(int *arr,int len)
{
    int i;
    if(arr==NULL || len<=1)
        return;
    build_maxheap(arr,len);
    
    for(i=len-1;i>=1;--i){
        swap(&arr[0],&arr[i]);
        max_heapify(arr,0,--len);
    }
}
*/
 
 
int main()
{
    int arr[10]={91,8,6,82,15,18,7,46,29,12};
    int i;
    k_min(arr,10,4);
    for(i=0;i<10;++i)
        printf("%d ",arr[i]);
    system("pause");
}

思路4的实现

首先给出《编程之美》上给出的伪代码:

Kbig(S, k):
if(k <= 0):
return [] // 返回空数组
if(length S <= k):
return S
(Sa, Sb) = Partition(S)
return Kbig(Sa, k).Append(Kbig(Sb, k – length Sa)

Partition(S):
Sa = [] // 初始化为空数组
Sb = [] // 初始化为空数组
Swap(s[1], S[Random()%length S]) // 随机选择一个数作为分组标准,以
// 避免特殊数据下的算法退化,也可
// 以通过对整个数据进行洗牌预处理
// 实现这个目的
p = S[1]
for i in [2: length S]:
S[i] > p ? Sa.Append(S[i]) : Sb.Append(S[i])
// 将p加入较小的组,可以避免分组失败,也使分组
// 更均匀,提高效率
length Sa < length Sb ? Sa.Append(p) : Sb.Append(p)
return (Sa, Sb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Kbig(S, k):  
     if(k <= 0):  
          return []     // 返回空数组  
     if(length S <= k):  
          return S  
     (Sa, Sb) = Partition(S)  
     return Kbig(Sa, k).Append(Kbig(Sb, k – length Sa)  
  
Partition(S):  
     Sa = []            // 初始化为空数组  
     Sb = []        // 初始化为空数组  
     Swap(s[1], S[Random()%length S])   // 随机选择一个数作为分组标准,以  
                        // 避免特殊数据下的算法退化,也可  
                       // 以通过对整个数据进行洗牌预处理  
                        // 实现这个目的  
     p = S[1]  
     for i in [2: length S]:  
         S[i] > p ? Sa.Append(S[i]) : Sb.Append(S[i])  
                            // 将p加入较小的组,可以避免分组失败,也使分组  
                           // 更均匀,提高效率  
     length Sa < length Sb ? Sa.Append(p) : Sb.Append(p)  
     return (Sa, Sb)  

一个简化实现的如下:

#include <stdio.h>
#include <stdlib.h>

void swap(int *a,int *b)
{
*a=*a^*b;
*b=*a^*b;
*a=*a^*b;
}
/*
为了简单起见,这里只是单纯的选取第一个元素作为枢纽元素。这样选取枢纽,就难避免使得算法容易退化。
*/
int k_big(int arr[],int low,int high,int k)
{
int pivot = arr[low];
int high_tmp = high;
int low_tmp = low;
while(low < high){
//从右向左查找,直到找到第一个小于枢纽元素为止
while (low < high && arr[high] >= pivot)
{
--high;
}
arr[low] = arr[high];
//从左向右查找,直到找到第一个大于枢纽元素为止
while (low < high && arr[low] <= pivot)
{
++low;
}
arr[high] = arr[low];
}
arr[low] = pivot;

if (low == k - 1)
{
return arr[low];
}else if(low > k - 1)
{
return k_big(arr,low_tmp,low-1,k);
}else
{
return k_big(arr,low+1,high_tmp,k);
}
}
int main()
{
int arr[10]={-91,0,6,82,15,18,7,46,-29,12};
int i;
k_big(arr,0,9,4);
for(i=0;i<10;++i)
printf("%d ",arr[i]);
system("pause");
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    #include <stdio.h>
    #include <stdlib.h>
 
    void swap(int *a,int *b)
    {
        *a=*a^*b;  
        *b=*a^*b;  
        *a=*a^*b;  
    }
    /*
    为了简单起见,这里只是单纯的选取第一个元素作为枢纽元素。这样选取枢纽,就难避免使得算法容易退化。
    */
    int k_big(int arr[],int low,int high,int k)
    {
        int pivot  = arr[low];
        int high_tmp = high;
        int low_tmp = low;
        while(low < high){
            //从右向左查找,直到找到第一个小于枢纽元素为止
            while (low < high && arr[high] >= pivot)
            {
                --high;
            }
            arr[low] = arr[high];
            //从左向右查找,直到找到第一个大于枢纽元素为止
            while (low < high && arr[low] <= pivot)
            {
                ++low;
            }
            arr[high] = arr[low];
        }
        arr[low] = pivot;
 
        if (low == k - 1)
        {
            return arr[low];
        }else if(low > k - 1)
        {
            return k_big(arr,low_tmp,low-1,k);
        }else
        {
            return k_big(arr,low+1,high_tmp,k);
        }
    }
    int main()
    {
        int arr[10]={-91,0,6,82,15,18,7,46,-29,12};
        int i;
        k_big(arr,0,9,4);
        for(i=0;i<10;++i)
            printf("%d ",arr[i]);
        system("pause");
    }

题目描述

输入n个整数,输出其中最小的k个。

分析与解法

解法一

要求一个序列中最小的k个数,按照惯有的思维方式,则是先对这个序列从小到大排序,然后输出前面的最小的k个数。

至于选取什么的排序方法,我想你可能会第一时间想到快速排序(我们知道,快速排序平均所费时间为 n*logn ),然后再遍历序列中前k个元素输出即可。因此,总的时间复杂度: O(n * log n)+O(k)=O(n * log n)

解法二

咱们再进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序,即:

1、遍历n个数,把最先遍历到的k个数存入到大小为k的数组中,假设它们即是最小的k个数;
2、对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为

O(k)

);

3、继续遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与kmax比较:如果

x < kmax

,用x替换kmax,并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果

x >= kmax

,则继续遍历不更新数组。

每次遍历,更新或不更新数组的所用的时间为

O(k)



O(0)

。故整趟下来,时间复杂度为

n*O(k)=O(n*k)

解法三

更好的办法是维护容量为k的最大堆,原理跟解法二的方法相似:

  • 1、用容量为k的最大堆存储最先遍历到的k个数,同样假设它们即是最小的k个数;
  • 2、堆中元素是有序的,令k1<k2<...<kmax(kmax设为最大堆中的最大元素)
  • 3、遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与堆顶元素kmax比较:如果

    x < kmax

    ,用x替换kmax,然后更新堆(用时logk);否则不更新堆。

这样下来,总的时间复杂度:

O(k+(n-k)*logk)=O(n*logk)

。此方法得益于堆中进行查找和更新的时间复杂度均为:

O(logk)

(若使用解法二:在数组中找出最大元素,时间复杂度:

O(k))

解法四

在《数据结构与算法分析--c语言描述》一书,第7章第7.7.6节中,阐述了一种在平均情况下,时间复杂度为

O(N)

的快速选择算法。如下述文字:

  • 选取S中一个元素作为枢纽元v,将集合S-{v}分割成S1和S2,就像快速排序那样

    • 如果k <= |S1|,那么第k个最小元素必然在S1中。在这种情况下,返回QuickSelect(S1, k)。
    • 如果k = 1 + |S1|,那么枢纽元素就是第k个最小元素,即找到,直接返回它。
    • 否则,这第k个最小元素就在S2中,即S2中的第(k - |S1| - 1)个最小元素,我们递归调用并返回QuickSelect(S2, k - |S1| - 1)。

此算法的平均运行时间为O(n)。

示例代码如下:

//QuickSelect 将第k小的元素放在 a[k-1]
void QuickSelect( int a[], int k, int left, int right )
{
int i, j;
int pivot; if( left + cutoff <= right )
{
pivot = median3( a, left, right );
//取三数中值作为枢纽元,可以很大程度上避免最坏情况
i = left; j = right - 1;
for( ; ; )
{
while( a[ ++i ] < pivot ){ }
while( a[ --j ] > pivot ){ }
if( i < j )
swap( &a[ i ], &a[ j ] );
else
break;
}
//重置枢纽元
swap( &a[ i ], &a[ right - 1 ] ); if( k <= i )
QuickSelect( a, k, left, i - 1 );
else if( k > i + 1 )
QuickSelect( a, k, i + 1, right );
}
else
InsertSort( a + left, right - left + 1 );
}

这个快速选择SELECT算法,类似快速排序的划分方法。N个数存储在数组S中,再从数组中选取“中位数的中位数”作为枢纽元X,把数组划分为Sa和Sb 俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的 k-|Sa|个元素,这种解法在平均情况下能做到 O(n) 的复杂度。

更进一步,《算法导论》第9章第9.3节介绍了一个最坏情况下亦为O(n)时间的SELECT算法,有兴趣的读者可以参看。

举一反三

1、谷歌面试题:输入是两个整数数组,他们任意两个数的和又可以组成一个数组,求这个和中前k个数怎么做?

分析:

 “假设两个整数数组为A和B,各有N个元素,任意两个数的和组成的数组C有N^2个元素。
那么可以把这些和看成N个有序数列:
A[1]+B[1] <= A[1]+B[2] <= A[1]+B[3] <=…
A[2]+B[1] <= A[2]+B[2] <= A[2]+B[3] <=…

A[N]+B[1] <= A[N]+B[2] <= A[N]+B[3] <=…
问题转变成,在这N^2个有序数列里,找到前k小的元素”

2、有两个序列A和B,A=(a1,a2,...,ak),B=(b1,b2,...,bk),A和B都按升序排列。对于1<=i,j<=k,求k个最小的(ai+bj)。要求算法尽量高效。

3、给定一个数列a1,a2,a3,...,an和m个三元组表示的查询,对于每个查询(i,j,k),输出ai,ai+1,...,aj的升序排列中第k个数。

寻找最小(最大)的k个数的更多相关文章

  1. 寻找最大(小)的K个数

    <<编程之美>>一书中提到了寻找最大的K个数的问题,问题可以简单描述为:在长度为N的数组中,寻找第K(K<N)个最大的数.问题的解法涉及到了很多排序算法,对我们理解和运用 ...

  2. 找出一堆数中最小的前K个数

    描写叙述: 给定一个整数数组.让你从该数组中找出最小的K个数 思路: 最简洁粗暴的方法就是将该数组进行排序,然后取最前面的K个数就可以. 可是,本题要求的仅仅是求出最小的k个数就可以,用排序能够但显然 ...

  3. 数组 寻找最大的第k个数

    int Partion(int asy[],int begin,int end) { int k=begin; int key=asy[end]; for(int i=begin;i<=end; ...

  4. 找到n中最小的k个数

    题目:n个数中,求最小的前k个数. 这道题在各个地方都看到过,在国内出现的频率也非常高. 面完阿里回来听说这道题又被考了,所以还是决定回来写一写,对于这种高频题...顺便再吐槽一下阿里的面试,我竟然一 ...

  5. 【1】TOPK最小的K个数(多种方法比较)

    (头条) 最小的第K个数也是和这题topK一样的思路 1.全排序  时间复杂度O(nlogn) 2.Partiton思想 时间复杂度O(n)  (因为不需要像快排一样对所有的分段都两两Partitio ...

  6. 排序矩阵中的从小到大第k个数 · Kth Smallest Number In Sorted Matrix

    [抄题]: 在一个排序矩阵中找从小到大的第 k 个整数. 排序矩阵的定义为:每一行递增,每一列也递增. [思维问题]: 不知道应该怎么加,因为不是一维单调的. [一句话思路]: 周围两个数给x或y挪一 ...

  7. 华为OJ2051-最小的K个数(Top K问题)

    一.题目描述 描述: 输入n个整数,输出其中最小的k个. 输入: 输入 n 和 k 输入一个整数数组 输出: 输出一个整数数组 样例输入: 5 2 1 3 5 7 2 样例输出: 1 2 二.Top ...

  8. 算法练习:寻找最小的k个数

    参考July的文章:http://blog.csdn.net/v_JULY_v/article/details/6370650 寻找最小的k个数题目描述:查找最小的k个元素题目:输入n个整数,输出其中 ...

  9. 算法笔记_035:寻找最小的k个数(Java)

    目录 1 问题描述 2 解决方案 2.1 全部排序法 2.2 部分排序法 2.3 用堆代替数组法 2.4线性选择算法   1 问题描述 有n个整数,请找出其中最小的k个数,要求时间复杂度尽可能低. 2 ...

随机推荐

  1. php把时间戳转换成英文格式

    <?php echo "时间格式1:".date("Y-m-d H:i:s ")."<br>";// 2010-06-12 ...

  2. ubuntu下使用sublime text进行C编程开发尝鲜

    1 选择编译系统 2 编写文件,编译(Ctrl+B)运行(Shift+Ctrl+B)

  3. JS怎样捕获浏览器关闭时间弹出自定义对话框

    <script type="text/javascript">window.onbeforeunload = function (e) { e = e || windo ...

  4. 超全面的JavaWeb笔记day13<JSTL&自定义标签>

    1.JSTL标签库(重点) core out set remove url if choose when otherwise forEach fmt formatDate formatNumber 2 ...

  5. Extjs学习笔记--(六,选择器)

    文档对象dom是javascript与页面元素的桥梁 选择器的作用就是通过元素的标签名,属性名,css属性名对页面进行快速,准确的定位及选择 Extjs的选择器:Ext.DomQuery Ext.qu ...

  6. swift -- 计步器CMPedometer的使用

    最近公司接了个项目,是一款运动类型的APP,可以检测运动量(例如:步数,上下楼等).睡眠信息.速度等信息,因为以前粗略的了解过传感器方面的相关信息,知道主要是苹果设备内置的传感器在起作用,传感器的种类 ...

  7. Nginx 链接

    Nginx反向代理以及负载均衡配置:http://www.cnblogs.com/Miss-mickey/p/6734831.html

  8. lodash(二)对象+循环遍历+排序

    前言: lodash(一)中只是研究了array中的多种方法,接下来就是经常用到的循环遍历问题 过程: 1._.forEach(collection, [iteratee=_.identity], [ ...

  9. (转)关于android设备管理器的一些分析

    转自http://bbs.pediy.com/showthread.php?t=183692 想必很多人都知道轰动一时android木马OBAD,该木马利用android设备管理器的漏洞,当用户激活设 ...

  10. 【HTML】改变鼠标样式图片css

    你需要一张图   .ico 的 格式 如果一开始你要解决的是怎么去用png 格式图片转成 ICO格式 先做一张32*32的PNG格式图片 然后 打开http://www.easyicon.net/co ...