一、题目打卡
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_if
、std::remove_if
、std::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 表达式的具体实现。常见的一些用法:
- 没有捕获、参数和返回值的 Lambda 表达式:
[] { std::cout << "Hello, Lambda!" << std::endl; }();
- 带参数的 Lambda 表达式:
[](int x, int y) { std::cout << "Sum: " << x + y << std::endl; }(5, 3);
- 带返回值的 Lambda 表达式:
int result = [](int x, int y) -> int { return x + y; }(5, 3); std::cout << "Result: " << result << std::endl;
- 捕获外部变量的 Lambda 表达式:
int x = 5; auto func = [x](int y) { std::cout << "Sum: " << x + y << std::endl; }; func(3);
- 通过引用捕获外部变量的 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) 的时间复杂度。