热门标签 | HotTags
当前位置:  开发笔记 > 人工智能 > 正文

从n个数中提取最小的m个数的算法

从n个数中提取最小的m个数的算法2007-02-1023:58:45分类:经常在网上看到有人讨论这
  从n个数中提取最小的m个数的算法  2007-02-10 23:58:45

分类:

经常在网上看到有人讨论这个问题:

如何高效地从n个数中提取最小的m个数?

或者是其他类似的问题,今天我也简单地分析一下。

具体问题具体分析,既然这个题目只要求我们找出这m个数,没有要求对其进行排序,所以负担也就轻了,相应地也能采用更高效的数据结构和算法。如果不要求空间复杂度,并且m不大,我们可以开辟另外一个空间(S)存储这m个数,一般的时候空间复杂度要求都是较低的,所以我们也可以这样假设。n个数中的前m个数我们可以直接放在空间S中,当取第m + 1个数的时候,我们就要考虑这个数是否要加入到空间S中,如果加入,应该遵循一个什么样的替换规则。我们需要找出的是最小的m个数,所以这m个数中最大的数M就是基准,如果后续的数比M大,那么就不应该加入空间,如果比M小,就要加入空间。当新数N需要加入空间时,被挤掉的数肯定是先前最大的数M,那么新数应该放在哪个位置呢?复杂度集中在如何找出最大的数M和如何插入新数N。其实,这两个问题是相关的,焦点就积聚在搜索最大数据和插入新数据的操作上。也许大家已经想到了,最大堆不就正适合此种情况吗?其最大数就是根元素,查找的时间复杂度为O(1),新数据的插入时间复杂度为O(log(n)),已经为理论上的最优解。

C++的程序源码:


#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>

using namespace std;

int main(int argc, char *argv[])
{
        vector<int> val, val2, val3;
        vector<int>::iterator it;
        int m = 3, n = 100, t;

        srand(time(NULL));
        for (int i = 0; i < n; i ++) {
                t = random();
                val2.push_back(t);
                val3.push_back(t);
                cout << t << " ";
        }
        cout << endl;

        for (int i = 0; i < n; i ++) {
                int t = val3[i];

                if (val.size() < m) {
                        val.push_back(t);
                        push_heap(val.begin(), val.end());
                        continue;
                }
                if (>= val[0])
                        continue;
                pop_heap(val.begin(), val.end());
                val[- 1] = t;
                push_heap(val.begin(), val.end());
        }

        cout << "Top " << m << ":" << endl;
        for (int i = 0; i < m; i ++)
                cout << val[i] << " ";
        cout << endl;

        sort_heap(val2.begin(), val2.end());
        cout << "Sorted Top " << m << ":" << endl;
        for (int i = 0; i < m; i ++)
                cout << val2[i] << " ";
        cout << endl;

        return 0;
}


代码很简单,如果你足够细心你会发现这个算法的实际时间复杂度为:

n * 2 * log ( m )

为什么多了系数2呢?因为pop_heap和push_heap的时间复杂度都为log(m),且每次空间S的更新操作都需要做这两步。再次考察这两个操作,如果你熟悉heap,就会发现pop_heap和push_heap两步可以合并成一步,请看pop_heap的主要步骤:
  1. 将根元素取下来。
  2. 将末尾的元素取下来。
  3. 从根开始搜索将第2步取下的元素插入到堆中的适当位置。
因为我们在pop之后马上就需要再次push,所以两步可以合并为:
  1. 将根元素去下来。
  2. 从根开始搜索将要push的元素插入到堆中的适当位置。
具体代码请看客自己实现吧,不要太懒了,懒惰可不是什么好毛病!

另外,C++的STL也有相关算法模板:

template <class RandomAccessIterator>
void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last);

template <class RandomAccessIterator, class StrictWeakOrdering>
void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last, StrictWeakOrdering comp);


用其改写的上述代码简单了很多:

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>

using namespace std;

int main(int argc, char *argv[])
{
        vector<int> val, val2;
        int m = 3, n = 100, t;

        srand(time(NULL));
        for (int i = 0; i < n; i ++) {
                t = random();
                val.push_back(t);
                val2.push_back(t);
                cout << t << " ";
        }
        cout << endl;

        nth_element(val.begin(), val.begin() + m - 1, val.end());
        cout << "Top " << m << ":" << endl;
        for (int i = 0; i < m; i ++)
                cout << val[i] << " ";
        cout << endl;

        sort(val2.begin(), val2.end());
        cout << "Sorted Top " << m << ":" << endl;
        for (int i = 0; i < m; i ++)
                cout << val2[i] << " ";
        cout << endl;

        return 0;
}


以上代码也算是nth_element应用的一个范例吧,至于它的具体实现,如果感兴趣还是自己分析,目前我也没有详细看。

推荐阅读
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • Søren Kierkegaard famously stated that life can only be understood in retrospect but must be lived moving forward. This perspective delves into the intricate relationship between our lived experiences and our reflections on them. ... [详细]
  • 本文详细探讨了Java中的24种设计模式及其应用,并介绍了七大面向对象设计原则。通过创建型、结构型和行为型模式的分类,帮助开发者更好地理解和应用这些模式,提升代码质量和可维护性。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 深入理解C++中的KMP算法:高效字符串匹配的利器
    本文详细介绍C++中实现KMP算法的方法,探讨其在字符串匹配问题上的优势。通过对比暴力匹配(BF)算法,展示KMP算法如何利用前缀表优化匹配过程,显著提升效率。 ... [详细]
  • 探讨一个显示数字的故障计算器,它支持两种操作:将当前数字乘以2或减去1。本文将详细介绍如何用最少的操作次数将初始值X转换为目标值Y。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 本文探讨如何设计一个安全的加密和验证算法,确保生成的密码具有高随机性和低重复率,并提供相应的验证机制。 ... [详细]
  • 深入解析:手把手教你构建决策树算法
    本文详细介绍了机器学习中广泛应用的决策树算法,通过天气数据集的实例演示了ID3和CART算法的手动推导过程。文章长度约2000字,建议阅读时间5分钟。 ... [详细]
  • 在金融和会计领域,准确无误地填写票据和结算凭证至关重要。这些文件不仅是支付结算和现金收付的重要依据,还直接关系到交易的安全性和准确性。本文介绍了一种使用C语言实现小写金额转换为大写金额的方法,确保数据的标准化和规范化。 ... [详细]
  • 在给定的数组中,除了一个数字外,其他所有数字都是相同的。任务是找到这个唯一的不同数字。例如,findUniq([1, 1, 1, 2, 1, 1]) 返回 2,findUniq([0, 0, 0.55, 0, 0]) 返回 0.55。 ... [详细]
  • 本文探讨了卷积神经网络(CNN)中感受野的概念及其与锚框(anchor box)的关系。感受野定义了特征图上每个像素点对应的输入图像区域大小,而锚框则是在每个像素中心生成的多个不同尺寸和宽高比的边界框。两者在目标检测任务中起到关键作用。 ... [详细]
  • 网络攻防实战:从HTTP到HTTPS的演变
    本文通过一系列日记记录了从发现漏洞到逐步加强安全措施的过程,探讨了如何应对网络攻击并最终实现全面的安全防护。 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
author-avatar
嘉洲仔-
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有