算法小白学习日记-5:堆

分享4道关于堆的题目,欢迎批评指正~


题目1:数据流中的第K大元素(leetcode-703)

题目描述

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

    样例输入

    ["KthLargest", "add", "add", "add", "add", "add"]
    [[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]

    样例输出

    [null, 4, 5, 5, 8, 8]

    题目分析

    统计一组数据的第K大元素,我们只需要记录前K大元素,不需要记录所有数据。而第K大元素,在前K大元素中,是最小的。因此可以通过小顶堆实现本题目。

    依次遍历每一个数据,若输入数据数量小于K,则直接入堆;若数据数量大于等于K,如果当前数据大于堆顶元素,则弹出堆顶元素,并将当前元素入堆。

    遍历完成后,返回堆顶元素。

    最终代码

    class KthLargest {
    public:
        typedef pair PII;//利用pair类型,防止set吞掉相同元素
        int tot = 0, k;
        set s;
        KthLargest(int k, vector& nums) {
            this->k = k;
            for(auto x : nums){
                add(x);//通过调用add实现数据的添加
            }
            return ;
        }
        
        int add(int val) {
            if(s.size() < k) {
                s.insert(PII(val, tot++));
            } else{
                if(s.begin()->first < val){
                    s.erase(s.begin());
                    s.insert(PII(val, tot++));
                }
            }
            return s.begin()->first;
        }
    };
    /**
     * Your KthLargest object will be instantiated and called as such:
     * KthLargest* obj = new KthLargest(k, nums);
     * int param_1 = obj->add(val);
     */

    题目2:数据流的中位数(leetcode-295)

    题目描述

    中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

    • 例如 arr = [2,3,4] 的中位数是 3 。
    • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。

      实现 MedianFinder 类:

      • MedianFinder() 初始化 MedianFinder 对象。

      • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

      • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

        样例输入

        ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
        [[], [1], [2], [], [3], []]

        样例输出

        [null, null, null, 1.5, null, 2.0]

        题目分析

        我们只关注数据中间的一个或两个数字,因此可以将数据从中间分成两段,左段比中位数小,右段比中位数大,前者为大顶堆,后者为小顶堆。我们通过维护,保证要么两个堆的数据数量相等,要么左段的堆的数据量多一个。具体流程如下:

        1. 当插入一个新的元素,首先根据元素的大小选择插入左右哪个堆:如果新的数据大于左堆的堆顶元素,则将数据插入右堆,否则插入左堆;
        2. 插入元素后,根据左右堆的元素数量,对两个堆进行维护;
        3. 最后根据左右两个堆的数量,计算中位数。

        最终代码

        class MedianFinder {
        public:
            typedef pair PII;
            int tot;
            set s1, s2;
            MedianFinder() {
                tot = 0;
            }
            
            void addNum(int num) {
                if(s1.size() == 0 || num < -s1.begin()->first){
                    s1.insert(PII(-num, tot++));
                } else{
                    s2.insert(PII(num, tot++));
                }
                int n1 = (s1.size() + s2.size() + 1) / 2;//根据两堆的元素数量进行维护
                if(s1.size() == n1) return ;
                if(s1.size() < n1) {
                    s1.insert(PII(-s2.begin()->first, tot++));
                    s2.erase(s2.begin());
                } else {
                    s2.insert(PII(-s1.begin()->first, tot++));
                    s1.erase(s1.begin());
                }
                return ;
            }
            
            double findMedian() {
                int t = s1.size() + s2.size();
                if(t % 2){
                    return -s1.begin()->first;
                }
                double a = -s1.begin()->first;
                double b = s2.begin()->first;
                return (a + b) / 2.0;
            }
        };
        /**
         * Your MedianFinder object will be instantiated and called as such:
         * MedianFinder* obj = new MedianFinder();
         * obj->addNum(num);
         * double param_2 = obj->findMedian();
         */

        题目3:丑树(leetcode-264)

        题目描述

        给你一个整数 n ,请你找出并返回第 n 个 丑数 。

        丑数 就是质因子只包含 2、3 和 5 的正整数。

        样例输入

        10

        样例输出

        12
        [1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

        题目分析

        本题的关键在于如何生成一个丑数序列,若某个数是丑数,则这个数的两倍、三倍、五倍也都是丑数。可以用小顶堆来存储丑数,每次弹出堆顶的元素,同时让堆顶元素的两倍、三倍、五倍入堆,迭代n次。

        为提高运算效率,可以对代码进行改进。按照上面的解法有可能产生重复的数,例如弹出2后,会将4、6、10入堆,下面在弹出3时,会让6、9、15入堆,次数6被入堆了两次。虽然set可以筛掉重复的数,但是重复操作会降低效率。

        因此,为了避免重复,在弹出某个数字后,首先求该数字的最大因子,当最大因子是2,则将这个数的2倍、3倍、5倍入堆;当最大因子是3,则将这个数的3倍和5倍入堆;当最大质因子是5,则将这个数的5倍入堆。这样就避免了冲入数字的入堆操作。(虽然会增加额外的判断和计算,但实测整体效率更高)

        最终代码

        class Solution {
        public:
            int nthUglyNumber(int n) {
                set s;
                s.insert(1);
                long long ans = 0;
                int flag = 0;
                while(n--) {
                    ans = *s.begin();
                    s.erase(s.begin());
                    if(ans % 5 == 0) flag = 2;//根据堆顶元素的最大因子,判断入堆的情况
                    else if(ans % 3 == 0) flag = 1;
                    else flag = 0;
                    switch (flag){
                        case 0: s.insert(ans * 2);
                        case 1: s.insert(ans * 3);
                        case 2: s.insert(ans * 5); break;
                    }
                }
                return ans;
            }
        };

        题目4:超市买货

        题目描述

        ​ 超市里有N个商品. 第i个商品必须在保质期(第di天)之前卖掉, 若卖掉可让超市获得pi的利润.

        ​ 每天只能卖一个商品.

        ​ 现在你要让超市获得最大的利润.

        样例输入

        每组数据第一行为一个整数N(0

        ​ 之后N行,每行各有两个整数, 第i行为pi,di(1<=pi,di<=10000)

        7
        20 1 
        2 1 
        10 3 
        100 2 
        8 2 
        5 20 
        50 10

        样例输出

         输出当前条件下超市的最大利润.

        185

        题目分析

        题目比较复杂,不知道该怎样下手,遇到这种情况可以对样例数据进行操作,从中找突破口。

        我们从第零天开始分析,首先肯定出售保质期短的商品,因为保质期长的商品可以未来出售。“20,1”和“2,1”两个商品的保质期都是一天,但前者利润更高,因此第一天售卖“20,1”。接下来是“100,2”和“8,2”,前者利润更高,因此 第二天售卖“100,2”。

        因此,可以按照保质期从短到长遍历每个商品,每卖出一个商品,天数加一。当遍历到第x个商品,如果该商品没有过保质期,则当天就买此商品;如果商品的保质期已过,为获得最大利润,可以对之前的售卖策略进行调整,如果当前商品的利润大于现有方案中利润最小商品的利润,则用当前商品替换利润最小的商品。

        最终代码

        #include #include using namespace std;
        int main(){
            int N, ans = 0;
            typedef pair PII;
            set D;//记录每个商品的保质期
            set ARR;//记录已售卖的商品
            cin >> N;
            int p[N], d[N];
            for(int i = 0; i < N; i++){
                cin >> p[i] >> d[i];
                D.insert(PII(d[i], i));
            }
            int num, di, pi;
            int day = 0;
            while(!D.empty()){
                num = D.begin()->second;
                di = D.begin()->first;
                pi = p[num];
                D.erase(D.begin());
                if(ARR.empty() || di > day) {
                    ans += pi;
                    day++;
                    ARR.insert(PII(pi, num));
                } else  if(ARR.begin()->first < pi){
                    ans -= ARR.begin()->first;
                    ans += pi;
                    ARR.erase(ARR.begin());
                    ARR.insert(PII(pi, num));
                }
                
            }
        	cout << ans << endl;
            return 0;
        }
        

        总结

        利用堆结构,可以方便高效的取出数据中的最大或最小值。在处理复杂的极值问题时,可以通过实际操作、枚举结果等方式,分析出解决思路,对问题进行简化。例如题目1、2中,并不需要记录所有的数据,只需要记录极值等部分信息。