上强度了,感觉又学到了很多新知识,看题解和问文心一言,也学到了基础语法。
今天的内容主要是单调队列和优先级队列,最开始我以为两者是一样的,但是实际上不一样。在这里列一下两者的区别。
单调队列和优先级队列在数据结构和特性上存在明显的区别。以下是两者的主要区别:
- 定义与特性:
- 单调队列:单调队列维护了一个具有单调性的队列。按照遍历顺序入队、出队,每个元素仅入队、出队一次。队列中的元素始终保持单调增或单调减的特性。队首和队尾都可以进行出队操作,但只有队尾可以进行入队操作。对于递增/非降队列,队首元素为最小值;对于递减/非升队列,队首元素为最大值。
- 优先级队列:优先级队列是一种允许操作的数据带有优先级的数据结构。出队列时,优先级高的元素先出队列。在Java中,
PriorityQueue
是优先级队列的一种实现,它基于堆数据结构,提供了返回最高优先级对象和添加新对象的基本操作。- 实现方式:
- 单调队列:单调队列通过双向队列(deque)实现,以便支持从队列的两端进行出队操作。
- 优先级队列:在Java中,
PriorityQueue
使用二叉堆(Heap)数据结构实现,确保队头元素(根节点)具有最高或最低优先级。- 单调性:
- 单调队列:元素始终保持单调增或单调减的特性。
- 优先级队列:元素本身不具有单调性,但根据优先级排序,优先级最高的元素始终位于队头。
- 元素关系:
- 单调队列:元素之间的关系具有单调性,即队首元素始终是最小值或最大值。
- 优先级队列:元素之间的关系基于优先级,而非单调性。
- STL操作:
- 单调队列:在STL(Standard Template Library)中,单调队列通常使用
deque
(双向队列)来实现。- 优先级队列:在STL中,
PriorityQueue
提供了优先级队列的实现。- 长度与容量:
- 单调队列:长度取决于输入数据的合法性。
- 优先级队列:长度始终与输入数据的数量等同,没有容量限制,可以插入任意多个元素。
- 时间复杂度:
- 单调队列:在每次加入或删除元素时都保持序列里的元素有序,但具体的时间复杂度取决于实现方式和操作。
- 优先级队列:
PriorityQueue
中插入和删除元素的时间复杂度通常为O(logN),其中N为队列中元素的数量。总结来说,单调队列和优先级队列在数据结构、实现方式、元素关系、STL操作、长度与容量以及时间复杂度等方面均存在显著差异。单调队列主要关注元素之间的单调性,而优先级队列则关注元素的优先级。
priority_queue
是 C++ 标准库中的一个容器适配器,它封装了另一个容器(通常是vector
、deque
或其他支持随机访问迭代器的容器),并提供了一个队列接口,但其中的元素不是按照它们被添加的顺序出队的,而是根据它们的优先级。priority_queue
的默认行为是一个最大堆,这意味着具有最高优先级的元素(即最大的元素)总是位于堆顶,并首先被出队(通过调用top()
和pop()
)。
priority_queue
有三个模板参数:
- 第一个模板参数是存储在队列中的数据类型。
- 第二个模板参数是底层容器类型,它必须支持随机访问迭代器(默认为
std::vector
)。- 第三个模板参数是比较函数对象,它决定了元素的排序方式(默认为
std::less<T>
,其中T
是存储在队列中的数据类型)(与快排相反)。在你提供的代码中,
priority_queue
被用作一个最大堆来存储整数对(由数字和其出现次数组成),并且使用了自定义的比较类mycomparison
来反转默认的排序顺序,使得出现次数最少的元素在堆顶。但是,实际上由于mycomparison
比较的是second
成员(即出现次数),因此这个优先队列实际上存储的是出现次数最多的元素在堆顶,即它是一个最大堆。下面是一个简化的
priority_queue
使用示例,演示了如何使用默认的最大堆行为:#include <iostream> #include <queue> #include <vector> int main() { std::priority_queue<int> pq; // 默认最大堆 pq.push(3); pq.push(1); pq.push(4); pq.push(1); pq.push(5); while (!pq.empty()) { std::cout << pq.top() << " "; // 输出:5 4 3 1 1(可能因实现而异) pq.pop(); } return 0; }
在这个例子中,
priority_queue
被用作一个最大堆,并且每次调用top()
都会返回当前堆顶的最大元素。每次调用pop()
都会移除堆顶元素,并重新调整堆以保持其属性。
在下面两道题中就分别用到了单调队列和优先级队列,让我一把子加深了了解。
注释都写好了,也能不看题解单独写出来了,应该是基本理解了。
这题主要是考察了单调队列的实现,之前做力扣的时候基本上很少做这种需要自己定义类的题,需要多练习。
class Solution {
private:
// 使用双向队列deque来实现MyQueue
class MyQueue {
public:
// 定义一个双向队列que
// 有pop_back()、pop_front()、push_back()、push_front()函数
// 双向都能插入和弹出
// 但是单向一般是队尾进队首出
deque<int> que;
// 返回队首元素
int front() {
return que.front();
}
// 单调队列的关键,当准备插入的时候,先判断一下要插入的值和队列的末尾的值的大小
void push(int val) {
while(!que.empty()&&val>que.back()) {
que.pop_back();
}
// 然后再插入
que.push_back(val);
}
// 当队列不为空,然后要弹出的值和队首的一样就可以弹出了
void pop(int val) {
if(!que.empty()&&val==que.front()) {
que.pop_front();
}
}
}; // 别忘记逗号
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int>result;
// 先把前k个元素放进去,防止待会的遍历nums[i-k]不在范围内
for(int i=0;i<k;i++) {
que.push(nums[i]);
}
result.push_back(que.front());
for(int i=k;i<nums.size();i++) {
que.pop(nums[i-k]);
que.push(nums[i]);
result.push_back(que.front());
}
return result;
}
};
这题主要是考察了优先级队列,这个在STL里已经有对应的PriorityDeque了,接着就是学习如何使用。这里最开始是定义了一个mycomparison,来帮助定义小顶堆,不然默认是大顶堆。
左侧大于右侧->小顶堆
右侧大于左侧->大顶堆
class Solution {
public:
class mycomparison{
public:
// operator()定义函数对象
// const可以防止改变值
// &可以直接访问对象,而不是以拷贝的形式,提高效率
bool operator()(const pair<int,int>& lhs,const pair<int,int>& rhs) {
return lhs.second>rhs.second; //小顶堆
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int>map; // 定义一个map来存储元素与元素出现的次数
vector<int>result(k); // 定义一个result存储答案,大小固定为k
for(int i=0;i<nums.size();i++) {
map[nums[i]]++;
}
// 定义优先队列
// 第一项是元素类型,第二项是原始的容器,第三项是排列规则
priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison> que;
// 按照刚刚定义的小顶堆que,来放对应的pair
// unordered_map<int,int>::iterator it 和 auto it是一样的
for(auto it=map.begin();it!=map.end();it++) {
que.push(*it);
if(que.size()>k) que.pop(); // que的size超出了题目要求的k就弹出
}
// 此时小顶堆里栈顶存的是偏小的数,所以要倒着放进答案里
for(int i=k-1;i>=0;i--) {
result[i]=que.top().first;
que.pop();
}
return result;
}
};
这里要补充的几个个人不熟悉的点:
- operator()定义函数对象:
- const &的使用:
- priority_queue<>内的参数
- map里的元素类型是pair<,>
- ::iterator it与auto it
大部分都在注释里写好了,不赘述了