zoukankan      html  css  js  c++  java
  • 寻找最大(小)的K个数

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

      1.解决方案

      解决思路一:我们首先可以想到的方法,先对数据进行排序,然后选择K个最大的值,算法时间复杂度O(N*logN) + O(K) = O(N*logN)。

          解决思路二:注意到题目要求造成K个最大的数,并没有要求这个K个最大的数是否有序。联想到快速排序算法,快速排序算法每一步可以讲数组分词两个部分,其中,一个部分的所有元素均要小于另一部分的所有元素。我们可以利用快速排序算法思想,分割数据,产生K个最大的原始,算法的步骤如下:两个部分

         1)随机选择pivot元素,将数组分成Sa与Sb两个部分,

         2)若Sb长度大于等于K,则返回Sb中寻找第K的最大元素;

       3)若Sb长度小于等于K,则返回Sa中第K-|Sb|个最大元素

    算法平均时间复杂度O(N*logK)。

      解决思路三:在N个元素中找出K个最大的元素,本质上与寻找K个最大的元素中最小的那个,我们假设这个元素是Vk,N个元素中最大的是Vmax,最小的元素是是Vmin。那么Vk,必然在[Vmin,Vmax]之间,因此我们可以使用二分查找排序的方法在[Vmin,Vmax]之间找出这个原则Vk。

      解决思路四:上面的三种解决方案,都需要将N个元素加载到内存,但如果N是一个非常大的值,比如超过4G空间,现有的内存空间加载这N个元素失败,造成三种算法失效。对于这个问题,我们可以考虑采用“在线更新”策略:维持K个元素,对于一个新的元素,如果比K个元素中最小元素小,则不更新当前的K个元素;如果新的元素要比K个元素中最小元素大,则更新这个K个原则。于是乎一种优美更新排序方法,堆排序算法,进入了我们的视野。

      解决思路五:直方图法,假设存在这样的一个直方图,该直方图的横坐标是序列中出现的元素,而纵坐标则是序列中改元素出现的次数。当我需要K个最大的元素,只需要沿着横坐标反方向,累加相应纵坐标相应的值,直到k为止。这个方法依赖于哈希结构。算法的时间复杂度O(N),然而算法需要额外的存储结构哈希表。

      2.基础算法实现

      上述解决方案中,依赖与基础算法:快速排序,堆排序,因此,我们首先实现这三个基础算法。

    •    堆排序算法实现:

      堆排序(heap sort)算法是一种非常优良的算法,特别在当今大数据的时代,亦更能体现其价值,奥地利符号计算研究所的Christoph Koutschan博士在自己的页面上发布一篇文章,选出了计算机科学中最重要的32个算法,快速排序位列其中。堆排序算法的平均时间复杂度O(N*logN)。

      首先简单回顾一下堆排序算法,堆排序算法依赖与二叉树定义的数据结构,它定义父节点的值均大于子节点的值。堆属于完全二叉树,可以将堆简单的映射到向量存储结构中,具有高效的实现效率,假设存储节点的下标从1开始,父节点下标记为i,则左子节点下标2*i,右子节点下标2*i + 1。此外涉及堆的操作包括:堆的构建、入堆,出堆已经堆排序。

      算法实现上,我们检查C++SLT中是否有heap数据结构,如果有,就可以直接拿过来用。可惜的是,STL中并没有把heap单独作为一种容器,而是把它作为其他容器的底层容器(如Priority Queue),但STL在泛型算法提高了构造heap堆的接口,我们可以依托vector与相关堆的算法,实现堆排序功能。

    //STL heap implement, main.cpp

    #include <iostream> #include <vector> #include <algorithm> using namespace std; int _tmain(int argc,_TCHAR* argv[]) { int myints[] = {10, 20, 30, 5, 15}; vector<int> v(myints, myints+5); make_heap (v.begin(),v.end()); //构建堆 cout << "initial max heap : " << v.front() << ' '; pop_heap (v.begin(), v.end()); //出堆,返回最大的元素,STL并没有删除该值,而是放在vector尾端 v.pop_back(); cout << "max heap after pop : " << v.front() << ' '; v.push_back(99); //入堆,调整堆结构 push_heap (v.begin(),v.end()); cout << "max heap after push: " << v.front() << ' '; sort_heap (v.begin(),v.end()); //强制排序 cout << "final sorted range :"; for (unsigned i=0; i<v.size(); i++) cout << ' ' << v[i]; cout << ' '; system("pause"); return 0; }

      例子源于这里略有改动,以上是STL“算法”的堆实现,用起来并不是很方便,因此,我们尝试直接实现堆数据结构。

    // heap.h
    #ifndef HEAP_H
    #define HEAP_H
    
    #include <vector>
    #include <iostream>
    #include <functional>
    #include <climits>
    
    using namespace std;
    
    const int start_index = 1;      //容器中堆元素起始索引
    
    template <typename T, typename Compare = less<T> >
    class Heap
    {
    public:
        Heap();
        void InitHeap(T *data,const int n);   //初始化操作
        void MakeHeap();                      //建堆
        void PushHeap(T elem);                //向堆中插入元素
        void PopHeap();                       //堆顶元素出堆
        vector<T> SortHeap();
        T Top();                              //从堆中取出堆顶的元素
        bool IsEmpty();                       //判读堆是否为空
        size_t size() const;
        void PrintHeap() const;
    
    private:
        void adjustHeap(int childTree, T adjustValue);  //调整子树
        void percolateUp(int holeIndex, T adjustValue); //上溯操作
    
    private:
        vector<T> heapDataVec;                           //存放元素的容器
        int heapSize;                                   //堆中元素个数
        Compare comp;                                    //比较规则
    };
    
    #endif
    //heap.cpp
    
    #include "heap.h"
    
    using namespace std;
    
    template <typename T, typename Compare>
    Heap<T,Compare>::Heap():heapSize(0)
    {
        heapDataVec.push_back(INT_MAX);
    }
    
    template <typename T, typename Compare>
    void Heap<T,Compare>::InitHeap(T *data, const int n)
    {
        for (int i = 0;i < n;++i)
        {
            heapDataVec.push_back(*(data + i));
            ++heapSize;
        }
    }
    
    template <typename T, typename Compare>
    size_t Heap<T,Compare>::size() const
    {
        return heapSize;
    }
    
    template <typename T, typename Compare>
    T Heap<T, Compare>::Top()
    {
        return heapDataVec[start_index];
    }
    
    template<typename T, typename Compare>
    bool Heap<T, Compare>::IsEmpty()
    {
        return heapSize == 0;
    }
    
    template<typename T, typename Compare>
    void Heap<T, Compare>::PrintHeap() const
    {
        for (int i = 1; i <= heapSize; i++)
            cout<< heapDataVec[i]<<" ";
        cout<<endl;
    }
    
    template <typename T, typename Compare>
    void Heap<T, Compare>::MakeHeap()
    {
        //建堆:从叶节点开始调整不断的调整堆
        if (heapSize < 2)
            return;
        
        int parent = heapSize / 2;  //第一个需要调整的子树的根节点多,起始下标从1开始
        while(1)
        {
            adjustHeap(parent,heapDataVec[parent]);
            if (start_index == parent)
                return;
    
            --parent;
        }
    }
    
    template <typename T,typename Compare>
    vector<T> Heap<T,Compare>::SortHeap()
    {
        //堆排序:堆中所以元素出堆的过程
    
        while(!IsEmpty())
            PopHeap();
    
        return vector<T>(heapDataVec.begin() + 1, heapDataVec.end());
    }
    
    template <typename T, typename Compare>
    void Heap<T,Compare>::PushHeap(T elem)
    {
        //将新元素添加到vector底部,并执行一次上溯
        heapDataVec.push_back(elem);
        ++heapSize;
        percolateUp(heapSize,heapDataVec[heapSize]);//执行一次上溯操作,调整堆,以使其满足最大堆的性质
    }
    
    template <typename T, typename Compare>
    void Heap<T,Compare>::PopHeap()
    {
        //将堆顶的元素放在容器的最尾部,然后将尾部的原元素作为调整值,重新生成堆
        T adjustValue = heapDataVec[heapSize];
        heapDataVec[heapSize] = heapDataVec[start_index];
        --heapSize;
        adjustHeap(start_index,adjustValue);
    }
    
    
    template <typename T,typename Compare>
    void Heap<T,Compare>::adjustHeap(int childTree,T adjustValue)
    {
        //调整以childTree为根的子树为堆
        int holeIndex = childTree;
        int secondChid = 2 * holeIndex + 1;                    //洞节点的右子节点(注意:起始索引从1开始)
    
        while(secondChid <= heapSize)
        {
            if (comp(heapDataVec[secondChid],heapDataVec[secondChid - 1]))
            {
                --secondChid;
            }
    
            heapDataVec[holeIndex] = heapDataVec[secondChid]; //令较大值为洞值
            holeIndex = secondChid;                           //洞节点索引下移
            secondChid = 2 * secondChid + 1;                  //重新计算洞节点右子节点
        }
        
        if (secondChid == heapSize + 1)                       //如果洞节点只有左子节点
        {
            heapDataVec[holeIndex] = heapDataVec[secondChid - 1];
            holeIndex = secondChid - 1;
        }
        
        heapDataVec[holeIndex] = adjustValue;  //将待调整值放入到叶子节点
        percolateUp(holeIndex,adjustValue);    //需要再执行一次上溯操作
    }
    
    
    template <typename T, typename Compare>
    void Heap<T,Compare>::percolateUp(int holeIndex,T adjustValue)
    {
        //上溯操作: 将新节点与其父节点进行比较,如果键值比其父节点大,就父子交换位置。
        //          直到不需要对换或直到根节点为止
        int parentIndex = holeIndex / 2;
        while(holeIndex > start_index && comp(heapDataVec[parentIndex],adjustValue))
        {
            heapDataVec[holeIndex] = heapDataVec[parentIndex];
            holeIndex = parentIndex;
            parentIndex /= 2;
        }
        heapDataVec[holeIndex] = adjustValue;
    }
    // main.cpp
    #include "heap.h"
    #include "heap.cpp"
    
    #include <iostream>
    
    using namespace  std;
    
    int main()
    {
        const int heap_size = 5;
        int data[heap_size] = {10,20,30,5,15};
        
        Heap<int> myHeap;
        myHeap.InitHeap(data, 5);
    
        cout<<"
    Before heap sort:"<<endl;
        myHeap.PrintHeap();
    
        myHeap.MakeHeap();
        cout<<"
    After make heap:"<<endl;
        myHeap.PrintHeap();
    
        cout<<"
    The top of heap:"<<myHeap.Top()<<endl;
    
        myHeap.PushHeap(40);
        cout<<"
    After push value, heap is:"<<endl;
        myHeap.PrintHeap();
    
        cout<<"
    Ater pop value, heap is:"<<endl;
        myHeap.PopHeap();
        myHeap.PrintHeap();
    
        vector<int> sortedHeap = myHeap.SortHeap();
        cout<<"
    Heap sort:";
        for (vector<int>::reverse_iterator riter = sortedHeap.rbegin(); riter != sortedHeap.rend(); riter++)
            cout<<*riter<<" ";
    
        system("pause");
        return 0;
    }

    程序的运行的程序的结果如下:

    •   快速排序算法实现

       快速排序算法在算法占有举足轻重的位置,在<<The Best of the 20th Century: Editors Name Top 10 Algorithms>>一文中,列举了20实际最伟大的算法,快速排序位列其中,很多人认为,快速排序算法是排序算法的首先,可惜的是最近列出的32个计算机的重要算法中,没有列出,不知为何。

      快速排序算法属于典型的快速分治算法:将原问题划分成更小的子问题,其中,每个更小的子问题都有着与原问题相同的解形式。假设待排序的数组A[p...r],快速排序算法的基本思路如下:

      1)分解:将A[p...r]分解成两个部分A[p...q]与A[q+1, r]两个部分,其中A[p...q]中的元素均小于A[q+1, r]中的元素

      2)子问题:两个子问题A[p...q-1]与A[q+1, r]的排序,与原问题形式相同,递归调用快速排序

      3)解的合并:子问题排序是直接在A中就地排序,因此无须合并结果,直接返回A,得到排序结果

      快速排序的实现版本有多种,主要的区别在于:1)起始轴元素的选择,国内教材倾向于选择序列的第一个元素,而<<算法导论>>选择序列的最后一个元素;2)分解问题时,切分序列所采用的遍历方法不同,有的倾向于双向的遍历方法(严版数据结构采用的方法),有的则使用单向的遍历方法(<<算法导论>>)采用的方法。本文给出个人认为较为优雅的实现方法((严版数据结构采用的方法)。

    #include <iostream>
    #include <algorithm>
    #include <functional>
    
    using namespace std;
    
    template<typename T, typename Compare>
    int partition(T* array, int low, int high, Compare cmp)
    {
      //分解算法,返回轴元素的位置 T privot_value
    = array[low]; while (low < high) { while((low < high) && (cmp(privot_value, array[high]))) //从高向低寻找小于轴元素的元素 --high; swap(array[low],array[high]); //算法执行第一次与轴元素交换,之后,与大于轴元素的元素交换 while ((low < high) && (cmp(array[low], privot_value))) //从低向高寻找大于轴元素的元素 ++low; swap(array[low], array[high]); } array[low] = privot_value; return low; } template<typename T, typename Compare> void quick_sort(T* array, int low, int high, Compare cmp = less<int>()) { int privot_loc = partition(array, low, high, cmp); //问题分解 partition(array, low, privot_loc - 1, cmp); //低半部分子问题求解 partition(array, privot_loc + 1, high, cmp); //高半部分子问题求解 } int main() { const int array_size = 5; int my_array[array_size] = {10, 20, 30, 5, 15}; cout<<"Befor sort:"<<endl; for (int i = 0; i != array_size; i++) cout<<my_array[i]<<" "; quick_sort(my_array, 0, array_size-1, less<int>()); cout<<"After sort:"<<endl; for (int i = 0; i != array_size; i++) cout<<my_array[i]<<" ";
      cout<<endl; system(
    "pause"); return 0; }

      

      快速排序算法的性能与轴元素的选择十分相关,在最坏情况下算法的时间复杂度O(N2),算法平均的时间复杂度0(N*logN)。一种简单产生轴元素方法即在序列随机产生轴元素,此外一些文献中会论述了如何产生轴元素,有兴趣的读者可以翻阅相关文献。

      3.寻找最大(小)排序元素C++实现

      在第一个部分提到的五种解决思路中,思路一,思路四直接利用第二部分的的基础算法可以实现,思路三实现较为简单,我们实现剩下的两种种方法。

      思路二的C++实现:

    template<typename T, typename Compare>
    T quick_select(T* array, int low, int high, int k, Compare cmp = less<int>())
    {
        int privot_loc = partition(array, low, high, cmp);
        int m = high - privot_loc + 1;
    
        if (m == k)
        {
            //如果当前轴在序列中的位置(从高到低)与K相同,
            //则直接返回轴元素
            return array[privot_loc];
        }
        else if (m < k)
        {
            //如果当前轴在序列中的位置(从高到低)小于k,
            //则在序列的低半部分,寻找剩下的k-m个最大值
            return quick_select(array, low, privot_loc - 1 , k-m, cmp);
            
        }
        else
        {
            //如果当前轴在序列中的位置(从高到低)大于k,
            //则在序列的高半部分,寻找剩下的k个最大值
            return quick_select(array, privot_loc + 1, high, k, cmp);    
        }
    
    }
    
    
    int main()
    {
        const int array_size = 10;
        int k =0;
        int my_array[array_size] = {10, 20, 30, 5, 15, 50, 23, 17, 90, 8};
        cout<<"Please enter the kth greater: ";
        cin>>k;
    
        int kth_value = quick_select(my_array, 0, array_size - 1, k, less<int>());
        cout<<"The kth greater value is "<<kth_value;
        system("pause");
        return 0;
    }

    quick_select调用了在在第二部分实现的partition部分,quick_select实现也与第二部分的quick_sort十分相似,只是多了域范围判断。

      思路五的C++实现:

    #include <iostream>
    #include <algorithm>
    #include <climits>
    
    using namespace std;
    
    int select_kth_max(int* array, int array_size, int k)
    {
        int* p_max = max_element(array, array+array_size);
        int* hash_tabel = new int[*p_max];
        memset(hash_tabel, 0 , sizeof(int) * (*p_max));
        int kth_value = INT_MIN;
    
        for (int i = 0; i != array_size; i++)
        {
            hash_tabel[array[i]]++;
        }
    
        int acc_num = 0;
        for (int i = (*p_max) -1; i >=0; i--)
        {
            acc_num += hash_tabel[i];
            if (acc_num >= k)
            {
                kth_value = i;
                break;
            }
        }
    
        return kth_value;
    
    }
    
    int main()
    {
        const int array_size = 10;
        int my_array[array_size] = {10, 20, 30, 5, 15, 50, 23, 17, 90, 8};
        int k ;
        cout<<"Please enter the kth greater: ";
        cin>>k;
    
        int kth_value = select_kth_max(my_array, array_size, k);
        cout<<"The kth greater value is "<<kth_value;
    
        system("pause");
        return 0;
    }

       

      4.参考文献

      书籍:<<编程之美>>、<<算法导论>>、<<数据结构(c语言版)>>(严蔚敏)

      网上资源: 编程艺术总结,http://blog.csdn.net/v_july_v/article/details/6543438

            C++参考网站,http://www.cplusplus.com/reference/algorithm/make_heap/

                         C++堆实现,http://blog.csdn.net/xiajun07061225/article/details/8553808

        ps:在算法实现上,使用了C++template技术,使用template遇到了一些小问题:

               1)vs编译器,不支持模块头文件与实现文件的分离,否则会报Link Erro错误,解决方法有两种:

          (1)模板的实现文件与头文件合在一起;

          (2)仍然分离模板的实现文件与头文件,但在包含模板头文件的同时也包括实现文件。

        2)C++类模板支持省略参数,但是C++模板函数并不支持默认参数(有点奇怪~)。

      

  • 相关阅读:
    java虚拟机字节码执行引擎
    java7 invokedynamic命令深入研究
    [转载]使用expect实现shell自动交互
    elasticsearch 聚合时 类型转换错误
    ES的关键端口
    CentOS6.5安装ganglia3.6
    Linux远程执行echo问题
    [转载]CentOS修改用户最大进程数
    elasticsearch新加入节点不能识别问题
    ssh免密码登录的注意事项
  • 原文地址:https://www.cnblogs.com/wangbogong/p/3139388.html
Copyright © 2011-2022 走看看