寻找最小的 k 个数

寻找最小的 k 个数

题目描述:

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

分析和解法:

解法一:排序输出

要求一个序列中最小的 k 个数,按照惯有的思维方式,则是先对这个序列从小到大排序,然后输出前面的最小的 k 个数。
当然,选取什么排序算法都是可以的。为了效率,我们可以使用快排来进行排序,然后再遍历序列中前k个元素输出即可。
源代码如下:

#include <iostream>
#include <cstdlib>

using namespace std;

int Compare(const void *elem1, const void *elem2)
{
    return *((int *)(elem1)) - *((int *)(elem2));
}

int main()
{
    int a[100];
    int i = 0;
    char ch;
    while((ch = cin.get()) != '\n')  //此处也可以用 cin.peek() ,只预读,不取出 
    {
        cin.unget();
        cin >> a[i++];
    }
    int k;
    cin >> k;
    qsort(a, i, sizeof(int), Compare);
    if (i >= k)
    {
        for (int j = 0; j < k; j++)
            cout << a[j] << " ";
        cout << endl;
    }
    else 
        cout << "error: i < k";
    return 0;
}

分析:时间复杂度为 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 ,则继续遍历不更新数组。

源代码如下:

#include <iostream>
#include <cstdlib>

using namespace std;

void Swap(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp; 
}

int FindKMAX(int a[], int k)
{
    for(int i = 1; i < k; i++)
        if (a[0] < a[i])  Swap(a[0], a[i]);
    return a[0];  // a[0] 是 KMAX 
}

int main()
{
    int a[100];
    int i = 0;
    char ch;
    while((ch = cin.get()) != '\n')  //此处也可以用 cin.peek() ,只预读,不取出 
    {
        cin.unget();
        cin >> a[i++];
    }
    int k;
    cin >> k;
    int KMAX = FindKMAX(a, k);
    for (int j = k; j < i; j++)
    {
        if (KMAX > a[j])
            Swap(a[0], a[j]);
        else 
            continue;
        KMAX = FindKMAX(a, k);
    }
    for (int j = 0; j < k; j++)
        cout << a[j] << " ";
    cout << endl;
    return 0;
}

分析:每次遍历,更新或不更新数组的所用的时间为 O(k) 或 O(0) 。故整趟下来,时间复杂度为 n * O(k)=O(n * k) 。这种方法是无序输出的,只关心最小的 k 个数,具体谁大谁小并不关心。

解法三:维护最大堆

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

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

源代码如下:

#include <iostream>
#include <cstdlib>

#define LEFT(i) (2 * (i) + 1)
#define RIGHT(i) (2 * ((i) + 1))

using namespace std;

void Swap(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp; 
}

//此函数把一颗二叉树中以 node 为根的子树变成最大堆。
//注意: 使用的前提条件是 node 节点的左右子树(如果存在的话)都是最大堆。
void MaxHeapify(int heap[], int heap_size, int node) 
{
    int l_child = LEFT(node);
    int r_child = RIGHT(node);
    int max_value = node;
    if (l_child < heap_size && heap[l_child] > heap[max_value])
        max_value = l_child;
    if (r_child < heap_size && heap[r_child] > heap[max_value])
        max_value = r_child;
    if (max_value != node)
    {
        Swap(heap[node], heap[max_value]);
        // 之后还要保证被交换的子节点构成的子树仍然是最大堆
        // 如果不是这个节点会继续"下沉",直到合适的位置
        MaxHeapify(heap, heap_size, max_value);
    }
}

void BuildMaxHeap(int heap[], int heap_size)
{
    if (heap_size < 2)
        return;
        
    int first_leaf = heap_size >> 1;   //first_leaf = heap_size / 2;
    int i;
    //从最后一个非叶子节点开始自底向上构建
    for (i = first_leaf - 1; i >= 0; i--)
        MaxHeapify(heap, heap_size, i); 
}

int main()
{
    int a[100];
    int i = 0;
    char ch;
    while((ch = cin.get()) != '\n')  //此处也可以用 cin.peek() ,只预读,不取出 
    {
        cin.unget();
        cin >> a[i++];
    }
    int k;
    cin >> k;
    BuildMaxHeap(a, k);
    for (int j = k; j < i; j++)
    {
        int KMAX = a[0];
        if (KMAX > a[j])
            Swap(a[0], a[j]);
        else 
            continue;
        MaxHeapify(a, k, 0);
    }
    for (int j = 0; j < k; j++)
        cout << a[j] << " ";
    cout << endl;
    return 0;
}

分析:这样下来,总的时间复杂度: O(k +(n - k)* log k)= O(n * log k) 。此方法得益于堆中进行查找和更新的时间复杂度均为: O(log k)(若使用解法二:在数组中找出最大元素,时间复杂度: O(k)) 。

特别注意:

  • cin.peek() 只预读,不提取流数据,并且不会跳过空格和回车
  • 还有一些更高效率的算法,并且也有一些可以提高上面算法的一些小的改进,有兴趣的可以看看下面的参考资料

参考资料:《编程之法》The Art of Programming By July
找最小的K个数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容