【LeetCode & 剑指offer刷题】查找与排序题2:40 最小的k个数(对应Kth Largest Element in an Array)

【LeetCode & 剑指offer 刷题笔记】目录(持续更新中...)

40 最小的k个数

题目描述

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,
 
/*
//暴力法:sort, O(nlogn)
//方法一:使用自带的stl函数
#include <algorithm>
using namespace std;
class Solution
{
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k)
    {
        if(k<=0 || k > input.size()) return vector<int>(); //处理非法输入
        nth_element(input.begin(), input.begin()+k-1, input.end());
        vector<int> result(input.begin(),input.begin()+k); //构造结果向量
        return result;
    }
};*/
/*
/*掌握
方法一:基于partition函数(快排中有用到,stl中也有,但是还是自己实现较好)
多次partition直到枢轴位置为k即可
缺点:会改变输入数组的元素位置
平均O(n),每次partition 平均O(n),次数未知,真的是O(n)吗,存疑?
思考:最好情况为O(n),最坏情况为O(n^2)(如对于倒序排列的数组)
通过优化partition,比如三数中值枢轴法或随机初始化枢轴法,可以改善时间复杂度
 
分析参考:
考虑最坏情况下,每次 partition 将数组分为长度为 N-1 和 1 的两部分,然后在长的一边继续寻找第 K 大,此时时间复杂度为 O(N^2 )。
不过如果在开始之前将数组进行随机打乱,那么可以尽量避免最坏情况的出现。
而在最好情况下,每次将数组均分为长度相同的两半,运行时间 T(N) = N + T(N/2),时间复杂度是 O(N)。
*/
#include <cstdlib>
class Solution
{
public :
    vector < int > GetLeastNumbers_Solution ( vector < int > input , int k )
    {
       if ( input . empty () || k <= 0 || k > input . size ()) return vector < int >(); //处理异常输入
      
        int left = 0 , right = input . size ()- 1 ;
        int pivot_pos ;
        while ( left <= right ) //类似二分查找法
        {
            pivot_pos = partition ( input , left , right ); //如果要求最大的第k个数,可以对partition函数进行改造
            if ( pivot_pos < k - 1 )
                left = pivot_pos + 1 ;
            else if ( pivot_pos > k - 1 )            
                right = pivot_pos - 1 ;
             else
                 break ; //此题要求的是返回最小的前k个数,如果仅返回最小的第k个数,直接在这里return a[pivot_pos]即可
                
        }
        vector < int > result ( input . begin (), input . begin ()+ k ); //构造结果向量
        return result ;
    }
private :
    int partition ( vector < int >& a , int left , int right )
    {
        //随机初始化枢轴 5ms
        //srand(time(nullptr)); //以当前时间为随机生成器的种子
        //int pivotpos = rand()%(right - left + 1) + left; //产生【left,right】之间的数
        //swap(a[pivotpos], a[left]); //将枢轴暂时放入起始位置
      
        int pivot = left ; //枢轴位置 4ms
      
        while ( left < right )
        {
            while ( left < right && a[right] >= a[pivot ]) right --; //找到本次扫描中第一个不满足枢轴规律的高位数
            while ( left < right && a[left] <= a[pivot ]) left ++; //找到本次扫描中第一个不满足枢轴规律的低位数
            swap ( a [ left ], a [ right ]); //交换以使满足枢轴规律
        } //最后结果是left和right均指向枢轴位置
        swap ( a [ left ],  a [pivot ]); //将枢轴移动到位
        return left ; //返回枢轴位置
    }
};
 
//
/*掌握
方法二:使用堆或者红黑树(平衡二叉搜索树)
用容器存储k个数,遍历输入向量过程中不断更新容器内的数(如果当前数小于容器中的最大值,则插入该数,删除原最大数)
优点:不需要修改输入数组,且适用于处理海量输入数据
O(nlogk)
*/
class Solution
{
public :
    vector < int > GetLeastNumbers_Solution ( vector < int > input , int k )
    {
        if ( input . empty () || k <= 0 || k > input . size ()) return vector < int >(); //处理异常输入
        //仿函数中的greater<T>模板,从大到小排序(默认从小到大,左结点<父结点<根结点)
        multiset < int , greater < int >> leastNums ; //用红黑树存储这k个数
        for ( int ai : input )
        {
            if ( leastNums . size () < k ) leastNums . insert ( ai ); //将前k个元素插入容器
            else
            {
                //第一个数为最大数
                multiset < int , greater < int >>:: iterator greatest_it = leastNums . begin ();
                //如果后续元素小于第一个元素,删除第一个,加入当前元素
                if ( ai < * greatest_it )
                {
                    leastNums . erase ( greatest_it ); //删除原最大值
                    leastNums . insert ( ai ); //插入新元素(logk复杂度)
                   
                }
            }
        }
       
       
        return vector<int>(leastNums.begin(), leastNums.end()); //返回结果向量(前k个最小的数)
    }
};
 
Kth Largest Element in an Array
Find the   k th largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
Example 1:
Input: [3,2,1,5,6,4] and k = 2
Output: 5
Example 2:
Input: [3,2,3,1,2,4,5,5,6] and k = 4
Output: 4
Note:  
You may assume k is always valid, 1 ≤ k ≤ array's length.

C++
 
 
暴力法:直接sort,O(nlogn),leetcode用时12ms
 
/*掌握
方法一:基于partition函数(快排中有用到,stl中也有,但是还是自己实现较好) 
多次partition直到枢轴位置为k即可
缺点:会改变输入数组的元素位置
 
leetcode 耗时4ms,若用pivot = left的做法,则耗时20ms
平均O(n),O(1)
每次partition 平均O(n),次数未知,真的是O(n)吗,存疑?
思考:最好情况为O(n),最坏情况为O(n^2)(如对于倒序排列的数组)
通过优化partition,比如三数中值枢轴法或随机初始化枢轴法,可以改善时间复杂度
*/
#include <cstdlib>
class Solution
{
public :
    int findKthLargest ( vector < int >& a , int k )
    {
       if ( a . empty () || k <= 0 || k > a . size ()) return 0 ; //处理异常输入
     
        int left = 0 , right = a . size ()- 1 ;
        int pivot_pos ;
        while ( left <= right ) //类似二分查找法
        {
            pivot_pos = partition ( a , left , right ); //如果要求最大的第k个数,可以对partition函数进行改造
            if ( pivot_pos < k - 1 )
                left = pivot_pos + 1 ;
            else if ( pivot_pos > k - 1 )           
                right = pivot_pos - 1 ;
            else
                return a [ pivot_pos ];
               
        }
       
    }
private :
    int partition ( vector < int >& a , int left , int right )
    {
        srand ( time ( nullptr )); //以当前时间为随机生成器的种子
        int pivotpos = rand ()%( right - left + 1 ) + left ; //产生【left,right】之间的数
        swap ( a [ pivotpos ], a [ left ]); //将枢轴暂时放入起始位置
     
        int pivot = left ; //枢轴位置
     
        while ( left < right )
        {
            //改造为从大到小partition,注意符号的变化
            while ( left < right && a [right] <= a[pivot ]) right --; //找到本次扫描中第一个不满足枢轴规律的高位数
            while ( left < right && a [ left ] >= a [ pivot ]) left ++; //找到本次扫描中第一个不满足枢轴规律的低位数
            swap ( a [ left ], a [ right ]); //交换以使满足枢轴规律
        } //最后结果是left和right均指向枢轴位置
        swap ( a [ left ], a [ pivot ]); //将枢轴移动到位
        return left ; //返回枢轴位置
    }
};
 
方法二:维护一个堆或者平衡二叉查找树存储这k个数
/*掌握
改进:维护一个大小为k的 小顶堆 ,扫描输入数据,不断更新小顶堆的内容
最后堆顶元素即可n个数中第k大的数
leetcode耗时4ms
 
每次堆调整平均时间复杂度为O(logk),共n次调整,故时间复杂度为O(nlogk)
O(nlogk), O(k)
*/
#include <algorithm>
#inlude <queue>
class Solution
{
public :
    int findKthLargest ( vector < int >& a , int k )
    {
        if ( a . empty () || k <= 0 || k > a . size ()) return 0 ; //处理非法输入(可依题目返回适当值)
       
        priority_queue < int , vector < int >, greater < int >> minheap ; //构建小顶堆
        for ( int i = 0 ; i < a . size (); i ++)
        {
            if ( i <= k - 1 )
                minheap . push ( a [ i ]); //将前k个元素插入容器
            else
            {
                if ( a[i] > minheap.top ()) //如果当前元素大于容器中最小的元素,则将该元素push进容器
                {
                    minheap . pop ();
                    minheap . push ( a [ i ]);//每次堆调整复杂度为O(logk)
                }
            }
        }
        return minheap . top (); //返回堆顶元素,即为n个数中第k大的数(如果要返回前k个数,需将最后的minheap全部pop)
     
    }
};
 
/*了解
用stl中make_heap函数,构建大顶堆,然后逐次输出堆顶元素 (原理与用priority_queue相同,不过没有额外空间)
用时11ms
O(nlogn + klogn),O(1)
*/
#include <algorithm>
class Solution
{
public :
    int findKthLargest ( vector < int >& nums , int k )
    {
        make_heap ( nums . begin (), nums . end ()); //构建大顶堆, 用nums存储,与下面区别就是节省了空间
        for ( int i = 0 ; i < k - 1 ; i ++)
        {
            pop_heap ( nums . begin (), nums . end ()); //将堆顶元素移至末尾,重新调整使(begin~end-1)的元素满足堆规律
            nums . pop_back (); //移除末尾元素
        }
       
        return nums [ 0 ];
           
       
    }
};
 
//用stl中sort函数,用时12ms
/*
#include <algorithm>
class Solution
{
public:
    int findKthLargest(vector<int>& nums, int k)
    {
        sort(nums.rbegin(), nums.rend()); //nums.rbegin()返回指向容器最后元素的逆向迭代器(因为sort默认按从小到大排序),
        //sort将rbegin()指向位置当做第一个元素,故可以实现从大到小排序
        return nums[k-1]; //第k个数,注意这里索引为k-1
       
    }
};
*/
/*
//用stl中nth_element函数。?为什么没有sort()或者堆排序快
//用时14ms
#include <algorithm>
class Solution
{
public:   
    int findKthLargest(vector<int>& nums, int k)
    {
        //nth_element(nums.begin(), nums.begin()+k-1, nums.end(),customMore);
        nth_element(nums.begin(), nums.begin()+k-1, nums.end(),greater<int>()); //原理为快排
        //这里直接用STL里的函数,比较函数设置为greater(默认为小数在前),注意中间(k-1)表示第k个最大的数
        return nums[k-1]; //第k个数,注意这里索引为k-1
       
    }
};
*/
/*
    // 用自定义函数对象排序
    struct
    {
        bool operator()(int a, int b) const
        {  
            return a > b;
        }  
    }customMore;
*/
 
相关:
Top K问题在数据分析中非常普遍的一个问题(在面试中也经常被问到),比如:
从20亿个数字的文本中,找出最大的前100个。
解决Top K问题有两种思路,
  • 最直观:小顶堆(大顶堆 -> 最小100个数), 该方法没有修改输入数据,且非常适合海量数据的输入,不用一次性读入内存,可以借助硬盘一边读一边处理,平均时间复杂度为O(nlogk)。
  • 较高效:基于partition函数的解法, 平均时间复杂度为O(n),但是会修改输入数组。
Quick Select(用快排中的partition函数)的目标是找出第k大元素,所以
  • 若切分后的左子数组的长度 > k,则第k大元素必出现在左子数组中;
  • 若切分后的左子数组的长度 = k-1,则第k大元素为pivot;
  • 若上述两个条件均不满足,则第k大元素必出现在右子数组中。
 
quick select实质是分治法
分治法 ,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较
 

猜你喜欢

转载自www.cnblogs.com/wikiwen/p/10225913.html
今日推荐