算法学习 | day11/60 滑动窗口最大值/前 K 个高频元素/栈总结(复习了谓词、lambda和运算符重载)

一、题目打卡

        1.1 滑动窗口的最大值       

        题目链接:. - 力扣(LeetCode)

        这个题目本身属于比较难的题目了,但是实际上主要还是考察单调队列的应用,并且需要自己手写一个单调的队列,之前做过一次这个题目,有一点模糊的印象,然后大概看了看解析写了第一版:

class Solution {
private:
    deque<int> d;

public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int i = 0, j = 1 - k;
        vector<int> res;
        for(;i < nums.size();i++){
            if(d.empty() || d.front() > nums[i]){
                while(!d.empty() && d.back() < nums[i]){
                    d.pop_back();
                }
                d.push_back(nums[i]);
            }else{
                d.push_front(nums[i]);
                while(d.back() < nums[i]){
                    d.pop_back();
                }
            }

            if(j >= 0 && j < nums.size()){
                res.push_back(d.front());
                if(d.front() == nums[j]) d.pop_front();
            }
            j++;
        }
        return res;
    }
};

        做完以后其实对单调队列的认知里,感觉它比较巧妙的一点是,如何处理在区间运行过程中一定不会成为最大值的那一堆元素,我的理解是,因为在单调队列加入新的元素以后,比这个元素要小的元素之所以要弹出,是因为在加入新元素的索引 - 1 的步长内,这些元素都不可能比这个新的元素要大,而且它们还是比这个新元素提前加入的,所以在这个步长内,他们只可能由于窗口的移动被弹出,所以它们不可能成为最大元素。        

        在实现上还是有很多细节需要注意,比如 j++ 摆放的位置,弹出元素的处理等,看了答案以后,发现答案对这个单调的队列进行了封装,而且没有必要像我这样分为 d.front() > nums[i] 和 d.front() < nums[i] 两种情况,实际插入的时候,始终在尾部插入就好,然后尝试优化了一版:

class myQueue{
private:    
    deque<int> d;
public:
    myQueue(){};
    void push(int val){
        if(d.empty()) d.push_back(val);
        else{
            while(!d.empty() && d.back() < val){
                d.pop_back();
            }
            d.push_back(val);
        }
    }
    
    void pop(){
        d.pop_front();
    }

    int front(){
        return d.front();
    }
};

class Solution {

public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        myQueue* m = new myQueue();
        vector<int> res;
        for(int i = 0, j = 1 - k; i < nums.size(); i++,j++){
            m->push(nums[i]);
            if(j >= 0){
                res.push_back(m->front());
                if(nums[j] == m->front()) m->pop();
            }
        }
        delete m;
        return res;
    }
};

        然后发现还是会有坑,这里最好还是声明两个变量 i 和 j 吧,不然对于类似 [1] k = 1 这种不好处理,然后就是别忘了释放内存。

        1.2 前 K 个高频元素(复习C++知识)

        这个题目做的时候有一定的想法,但是自己实现起来发现代码有很多地方都忘了怎么实现了,所以这样先贴一些自己复习的知识点:

谓词:

        在C++中,谓词(Predicate)是一个函数或函数对象,其主要目的是用于判断某种条件。谓词通常用于算法中,例如标准库算法 std::find_ifstd::remove_ifstd::sort 等。

        谓词可以是一个函数指针、函数对象、lambda 表达式等,只要它能够返回一个布尔值。这个布尔值表示谓词对输入值是否满足某种条件。

#include <iostream>
#include <vector>
#include <algorithm>

// 函数谓词
bool isEven(int num) {
    return num % 2 == 0;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 使用函数谓词
    auto evenIterator = std::find_if(numbers.begin(), numbers.end(), isEven);
    if (evenIterator != numbers.end()) {
        std::cout << "First even number found: " << *evenIterator << std::endl;
    }

    // 使用 lambda 表达式作为谓词
    auto oddIterator = std::find_if(numbers.begin(), numbers.end(), [](int num) {
        return num % 2 != 0;
    });
    if (oddIterator != numbers.end()) {
        std::cout << "First odd number found: " << *oddIterator << std::endl;
    }

    return 0;
}

lambda 表达式:

        匿名表达式的一般形式如下:

[capture](parameter_list) -> return_type {
    // 函数体
}
  • capture:捕获列表,用于指定 lambda 表达式中可以访问的外部变量。
  • parameter_list:参数列表,类似于函数的参数列表。
  • return_type:返回类型,指定 lambda 表达式的返回类型。
  • 函数体:Lambda 表达式的具体实现。

        常见的一些用法:

  1. 没有捕获、参数和返回值的 Lambda 表达式:
[] {
    std::cout << "Hello, Lambda!" << std::endl;
}();
  1. 带参数的 Lambda 表达式:
[](int x, int y) {
    std::cout << "Sum: " << x + y << std::endl;
}(5, 3);
  1. 带返回值的 Lambda 表达式:
int result = [](int x, int y) -> int {
    return x + y;
}(5, 3);
std::cout << "Result: " << result << std::endl;
  1. 捕获外部变量的 Lambda 表达式:
int x = 5;
auto func = [x](int y) {
    std::cout << "Sum: " << x + y << std::endl;
};
func(3);
  1. 通过引用捕获外部变量的 Lambda 表达式:
int x = 5;
auto func = [&x](int y) {
    x = x + y;
    std::cout << "Sum: " << x << std::endl;
};
func(3);
std::cout << "Modified x: " << x << std::endl;

运算符重载:

        基本语法:

return_type operator op(parameters) {
    // 实现运算符的具体逻辑
}
  • return_type:指定运算符的返回类型。
  • operator:关键字,用于指定运算符。
  • op:要重载的运算符。
  • parameters:运算符的参数。

        示例:

#include <iostream>

class Complex {
private:
    double real;
    double imag;

public:
    Complex(double r, double i) : real(r), imag(i) {}

    // 重载加法运算符
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // 打印复数
    void display() const {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(2.5, 3.0);
    Complex c2(1.5, 2.0);

    Complex result = c1 + c2; // 调用重载的加法运算符

    std::cout << "Result: ";
    result.display();

    return 0;
}

        看了答案以后,发现实现不是很困难,反而是在语法上的一些欠缺的知识让这个代码出现了很多错误:

class Solution {
public:
    class mycompare{
    public:
        bool operator()(const pair<int,int> &a,const pair<int,int> &b) const {
            return a.second > b.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> umap;
        for(auto &it : nums) umap[it]++;

        priority_queue<pair<int,int>, vector<pair<int,int>>, mycompare> p;

        for(auto & it:umap){
            p.push(it);
            if(p.size() > k) p.pop();
        }

        vector<int> res;
        while(!p.empty()){
            auto it = p.top();
            res.push_back(it.first);
            p.pop();
        }
        reverse(res.begin(),res.end());
        return res;
    }
};

        这里注意自己定义的mycompare的谓词,必须是public里面的,搜索了一下,也可以使用其他的方法:
 

// 普通函数
bool compare(const pair<int, int>& a, const pair<int, int>& b) {
    return a.second > b.second;
}

// 在 std::priority_queue 的实例化中使用 compare 函数
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&compare)> p(&compare);



// 结构体
struct MyCompare {
    bool operator()(const pair<int, int>& a, const pair<int, int>& b) const {
        return a.second > b.second;
    }
};

// 在 std::priority_queue 的实例化中使用 MyCompare 对象
priority_queue<pair<int, int>, vector<pair<int, int>>, MyCompare> p;



// 匿名函数
auto cmp = [](const pair<int, int>& a, const pair<int, int>& b) {
    return a.second > b.second;
};

priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> p(cmp);

        个人感觉匿名函数最简洁,这个题写完确实是复习了很多好久没有用过的知识点,想法也是,如果出现了键值需要根据值去反向获得键的情况时候,如果不想构建另一个哈希表,就需要利用谓词自己去设计比较的规则。

二、栈总结

        首先,在 C++ 中,常用的 stack 和 queue 并不是容器,而是容器适配器,其内部的内存是否连续取决于所使用的底层数据结构是什么样的,在默认缺省的情况下,底层使用的是 deque ,那么它就是不连续的。

        栈在底层的操作系统中也存在很广泛的应用,比如编译器处理括号、目录指令、递归等。

        对于题目而言,括号匹配、字符串去重、逆波兰表达式等都是经典的栈应用;求前 K 个高频元素、滑动窗口的最大值是典型的队列应用,特别是滑动窗口的最大值,进一步地需要自己根据题目的特点设计一个单调队列,同时在前k个高频元素中,也接触了优先级队列的数据结构,其本质上的实现是一个堆,而默认情况下 priority_queue 是一个大顶堆,也就是堆顶的元素比子节点的数都要大,底层上也就是一个完全二叉树,用它实现排序,可以做到 O(nlog k) 的时间复杂度。

  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值