算法小白学习日记-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<int, int> PII;//利用pair类型,防止set吞掉相同元素
    int tot = 0, k;
    set<PII> s;
    KthLargest(int k, vector<int>& 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<int, int> PII;
    int tot;
    set<PII> 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 个 丑数 。

丑数 就是质因子只包含 23 和 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<long long> 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≤10000), 即超市的商品数目

​ 之后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 <iostream>
#include <set>

using namespace std;

int main(){
    int N, ans = 0;
    typedef pair<int, int> PII;
    set<PII> D;//记录每个商品的保质期
    set<PII> 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中,并不需要记录所有的数据,只需要记录极值等部分信息。

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
k-means聚类算法是一种常用的无监督学习算法,用于将数据集划分为k个不同的簇。其参数包括k和max_iterations。 k代表聚类的数目,即要将数据集划分为多少个簇。在使用k-means算法之前,需要明确需要将数据划分为多少个簇。选择合适的簇数是一项重要任务,它直接影响到算法的性能和结果的准确性。根据实际问题的要求和数据的特征,可以通过调参或者利用先验知识来确定k的值。常用的确定簇数的方法有手肘法、轮廓系数和gap statistic等。 max_iterations是算法的最大迭代次数。k-means算法通过迭代的方式不断优化簇中心的位置,以获得更好的聚类效果。每一次迭代中,算法会计算每个样本点与各个簇中心的距离,将样本点划分到距离最近的簇中心所对应的簇中,并更新簇中心的位置。迭代直到达到最大迭代次数或者满足了早停条件,即簇中心的位置不再改变。 k-means算法的过程可以简述为以下几个步骤: 1. 随机选择k个样本作为初始的簇中心。 2. 计算每个样本点与各个簇中心的距离,将样本点划分到距离最近的簇中心对应的簇中。 3. 更新簇中心的位置,取簇中所有样本点的均值作为新的簇中心。 4. 重复步骤2和3,直到达到最大迭代次数或者满足早停条件。 5. 返回最后的簇划分结果。 总之,k-means聚类算法通过定义k个簇中心并迭代优化簇中心的位置,将数据集划分为k个不同的簇。通过调整k和最大迭代次数,可以控制聚类的精细度和算法的运行时间。它是一种简单而高效的聚类算法,在各个领域都有广泛的应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值