zoukankan      html  css  js  c++  java
  • 寻找一组数中最大的K个数

    对于"从一组数中挑出最大的K个数"这个在面试中经常会遇到,所以这次好好的去解析它,而当拿到这个问题时第一时间能想到解法就是:先对数据进行排序,然后再取最大的K个元素,当然这思路没毛病,但是对于数据量非常大(如:一万个数)的情况是不是先对它进行排序的代价太高了,有木有比较优的解法呢?当然有,那就是采用上篇学习到的利用二叉堆排序去解决。

    定义二叉堆结构:

    首先是需要用代码去实现一个堆这样的数据结构,而堆的特性在上篇【http://www.cnblogs.com/webor2006/p/7685197.html】中已经有介绍了,下面直接实现,先定义框架:

    其中核心就是实现数据的插入和删除,这个整个思想已经在之前介绍堆时介绍过了,这里就不多阐述了,下面来分别实现:

    ①、insert():插入元素

    其中怎么根据当前结点得到它的父结点的公式是:int parent = (pos - 1) / 2;猭由是根据父节点可以计算得到它左右子节点的位置,公式如:

    上面公式是在上篇对堆的理论上有详细说明,而其中的i就是父元素的位置,所以其推导过程,拿左子节点为例:

    因为:子节点位置 = 2 * 父结点位置 + 1

    所以:父结点位置parent = (子节点位置pos - 1) / 2

    接下来实现关键的对比部份,由于堆有小堆跟大堆,如下:

    所以这个结构为了考虑到这两种情况,将对比的情况写成抽象方法,由具体大小堆的类去实现这个对比条件,如下:

    //定义二叉堆结构
    #include <vector>
    #include <iostream>
    #include <algorithm>  //for swap
    
    template <typename T>
    class binary_heap
    {
        std::vector<T> data;//这里用系统的vector来存储,比数组要高级一点,而底层实际就是数组,会自动扩容
    protected:
      virtual bool compare(T a, T b) = 0;//定义虚方法,具体实现由子类决定
    public:
        //插入元素
        void insert(T value) {
            //首先将数据插入从最后插入
            data.push_back(value);
            //当前插入元素在集合中的位置
            int pos = data.size() - 1;
            //然后循环不断去找它的父结点进行大写对比
            while(pos > 0) {
                //获得父结点在集合中的位置
                int parent = (pos - 1) / 2;
                //用插入的元素跟父结点进行大小对比,并根据条件将当前元素与父结点进行交换
                if(parent < 0 || !compare(value, data[parent])) {
                    //退出循环条件:如果parent如果小于0则代表遍历结束了;如果对比条件不满足【大堆是当前值小于父结点、小堆是当前值大于父结点】
                    break;
                }
                //说明需要进行数据交换,采用系统库中的现有函数去做既可
                std::swap(data[pos], data[parent]);
                pos = parent;
            }
        }
    
        //取出根结点元素
        T root() {
            return data[0];
        }
    
        //删除堆中的根结点
        void delete_root() {
            //TODO
        }
    
        //判断数组是否为空
        bool empty() {
            return data.empty();
        }
    
        int size() {
            return data.size();
        }
    };
    
    template <typename T>
    class max_binary_heap : public binary_heap<T>
    {
    protected:
      virtual bool compare(T a, T b)
      {
        return a > b;
      }    
    };
    
    template <typename T>
    class min_binary_heap : public binary_heap<T>
    {
    protected:
      virtual bool compare(T a, T b)
      {
        return a < b;
      }    
    };

    这里为了能看出整个插入的一个排序结果,所以在完成排序之后,将当前元素输出出来:

    这就是整个的往堆中插入元素的方法,因为比较容易理解这里就不debug啦~

    ②、delete_root():删除根结点

    接着得实现第二个核心方法:从堆中删除根结点,这个在上篇的理论中了将整个删除的过程做了一个详细的阐述,所以这里直接按着其思路来实现它:

    接着拿左右子结点跟当前要删除的结点找出最大的数,如图:

    然后拿当前节点跟最大数进行对比,如果当前节点就是最大数则啥都不做递归结束,如果当前节点跟最大数不一样则进行数据交换如下:

    至上,整个二叉堆数据结构就已经实现啦,具体如何去使用继续往下看。

    完成堆排序:

    上面已经定义好了二叉堆的数据结构了,接着来用它完成关键的二叉堆的排序,如下:

    具体如何实现呢?有了二叉堆的数据结构了其实非常简单,首先先将待排序的数组元素都插入到堆中【这里用最大堆为例】:

    编译运行:

    如果想升序排列其实很简单,将最大堆换成最小堆既可,如下:

    编译运行:

    实现拿最大K元素:

    堆排序已经实现了,最后就到了这次的主题啦,怎么从大量数据集中拿最大的K个元素呢?

    先搭建框架:

    思考:是用最小堆还是最大堆去实现?

    假如采用最大堆去实现,因为它的根结点永远是最大的,那先将元素都插入到最大堆中,然后拿出k个根结点元素那不就是取出最大的k个元素啦,思路很正点,但是!!!在拿元素之前得先把元素中的所有元素都插入到堆中才行,假如有100万个数,那不这个最大堆的大小就得是100万这个大写,效率貌似不高,所以可以考虑用最小堆试试。

    假如采用最小堆,那可以先将前K个元素插入到最小堆中,然后再依次去跟K之外的元素一一跟根结点进行对比,如果比根结点要大则将根结点删除,将这个较大的数插入到最小堆中,也就是!!!永远堆中的数据个数只有K个,而不像最大堆那样堆需要N个元素的大小,这样用最小堆去实现优越感顺间就提升了,所以!这里采用最小堆来实现。

    具体如下:

    编译运行:

    这样就成功找到了最大的K元素啦~

    时间复杂度分析:

    所以说这里用最小堆实现的时间复杂度是T(n) = O(n * logk),其中n就是总元数个数,k则是要筛选的K个元素。

    但是如果用最大堆实现的话其复杂度就变成了O(n * log n)了,因为循环里面最坏的情况就得进行log n次对比,其中n肯定是比要比k要大得多,所以说用最小堆实现的算法是比较优的。

    而如果用最原始通过双层循环的方式来实现:最外层是k次循环,而最里层是n次循环,一个个元素进行大小比较,那这样的时间复杂度就是O(k * n)了,很明显这性能是最差的,虽说实现起来是最容易的。

  • 相关阅读:
    字符串函数---atof()函数具体解释及实现(完整版)
    curl的简单使用
    [7] 算法之路
    springMVC3.0(文件上传,@RequestMapping加參数,@SessionAttributes,@ModelAttribute,转发,重定向,数值获取,传參,ajax,拦截器)
    hdu 1754 I Hate It 线段树 点改动
    经典的7种排序算法 原理C++实现
    自己定义View实现水平滚动控件
    centos编译ffmpeg x264
    工作脚本处理文本
    A*寻路算法
  • 原文地址:https://www.cnblogs.com/webor2006/p/7686453.html
Copyright © 2011-2022 走看看