求数组中最小的k个数以及海量数据最大堆、multiset解决方案


【题目】

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。


【方案一】

主要有两种方案。第一是利用我们熟知的 partition 算法,它是快速排序的核心,相信每个人都会。它可以用来求取数组的任意第 k 大的数,时间复杂度是O(n)。我们不断对数据 partition,当它返回的 index 为第 k-1 是,那么就说明前 k 个数(包括 index对应的数)就是最小的 k 个数了。因为 index 对应数的左侧都比它小,一共 0~k-2 即 k-1 个,加上它自己,就是 k 个了。


代码:

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        const int size = input.size();
        vector<int> res;
        if(size == 0 || k <= 0 || k > size)
            return res;
        if(k == size)
            return input;
    
        int start = 0;
        int end = size - 1;
        int index = partition(input, start, end);
        while(index != k - 1){ 
            if(index > k - 1)
                end = index - 1;
            else
                start = index + 1;
            index = partition(input, start, end);
        }   

        for(int i=0; i<k; ++i)
            res.push_back(input[i]);
        return res;
    }   
private:
    int partition(vector<int>& arr, int start, int end){ 
        //int index = ( [start, end] (void)  //我试图利用随机法,但是这不是快排,外部输入不能保证end-start!=0,所以可能发生除零异常
        //              {return random()%(end-start)+start;} )(); 
        //std::swap(arr[start], arr[end]);

        int small = start - 1;
        for(int index=start; index<end; ++index){
            if(arr[index] < arr[end]){
                ++small;
                if(small != index)
                    std::swap(arr[small], arr[index]);
            }
        }
        ++small;
        std::swap(arr[small], arr[end]);
        return small;
    }
};



【方案二】

上面的 partition 算法有两个缺点,其一必须修改原数组元素(除非你拷贝出来,那也太蠢了),其二是不能针对海量数据。所以就有了最大堆的解法。我们用数组前 k 个元素建立 k 个节点的最大堆,后续输入如果小于最大堆的最大值,即头部,那么恭喜它,它入选了。然后把当前最大堆头部换成新元素,重新堆化。继续,循环,直到数据输入完毕。最大堆中剩余的 k 个元素即为所求。


代码:

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        const int size = input.size();
        vector<int> heap;
        if(size == 0 || k <= 0 || k > size)  //错误返回空
            return heap;
        if(k == size)   //大小相等直接返回
            return input;
        
       	heap.resize(k);  //不能用reserve
        for(int i=0; i<k; ++i)   //将前k个分配给堆
            heap[i] = input[i];
        
        for(int i=(k>>1)-1; i>=0; --i)
            sift_down(heap, i, k);   //建堆
        
        for(int i=k; i<size; ++i){   //遍历第[k+1..n],如果小于最大堆顶,就放入堆顶,然后重新堆化
        	if(input[i] < heap[0]){
                heap[0] = input[i];  //放入堆顶
            	sift_down(heap, 0, k);   //堆化
            }
        }
        
        return heap;
    }
private:
    int left_child(const int i){   //得到左孩子下标
        return (i << 1) + 1;
    }
    void sift_down(vector<int>& heap, int i, const int N){
		int tmp = heap[i];  //保存目标堆化元素
        for(int child = -1; left_child(i)<N; i=child){   //i=child
            child = left_child(i);
            if(child != N-1 && heap[child] < heap[child+1]) //找出左右孩子中较大的一个
                ++child;
            if(heap[child] > tmp)  //如果大于目标,那就让孩子节点覆盖自己
                heap[i] = heap[child];
            else
                break;
        }
        heap[i] = tmp; //这句话不能写在上面的break之上,因为有课能i是叶子节点,left_child(i)<N不会进入循环
    }
};


【方案三】

方案三和方案二理论是一样的,只不过使用了 STL 的 multiset,为什么不用 set 是因为 set 元素不可重复。由于 multiset 内部是红黑树,可以自动排序。我们使用 STL 个 greater<int> 让红黑树由大到小排序,红黑树的 begin() (不是一定头结点)就是最大值了。有了最大值剩下的就和方案二几乎一样了,不再赘述。


代码:

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        const int size = input.size();
        if(k <= 0 || k > size)
            return std::vector<int>();
        
        std::multiset<int, std::greater<int> > least_nums;
        for(auto v : input){
            if(least_nums.size() < k)
                least_nums.insert(v);
            else{
                auto begin = least_nums.begin();
                if(v < *begin){
                    least_nums.erase(begin);
                    least_nums.insert(v);
                }
            }
        }
        
        return std::vector<int>(least_nums.begin(), least_nums.end());
    }
};


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值