前言
之前因为种种原因还是把栈和队列分割了开来,所以今天把剩下的队列补完。
直奔主题利用头文件,即这两行代码引入数据结构即可:
#include<stack>
#include<queue>
#include<deque> // 双向队列
当然啦,如果想自己实现也挺简单的哈,可以参考我在第一篇文章推荐的教材或者自己另寻他法。
# 一、学习目标:
掌握队列的基本操作,善于从题目中判断出是否用到此种数据结构(即能应用解决实际问题)
队列的基本操作
和栈相同,首先是建立一个空队列:
queue<int> q; // 可以为任意Element Type
通过上面一行代码一个队列就建立好啦,其包含的操作下图所示:
不难看出其操作与栈有很大的相似性(忘了的可以翻看上一篇文章哈)。
事实上也确实如此,因此我们关注的point在于其特有的属性。
首先,队列和我们日常生活中排队的模型是一模一样的。试想,排队有先后顺序而且是符合先到先得的规则于是队列便是在此基础上衍生出来的先进先出的数据结构。一个队列有队头与队尾,这刚好对应了上面的front和back两个属性。测试图示如下所示:
易知最先放入的“LeBron James”是队头,而最后放入的“Stephen Curry”在队尾。
那么实际上关于队列的基本操作就结束了。
(怎么会!还有双向队列deque和优先队列priority_queue)
双向队列还是很好理解的,它更像是对队列的灵活性的一种补充。其操作数量就会复杂很多,这儿不可能去一一介绍。个人觉得最好用的还是可以从队列头填充元素,同时也可以从队尾先弹出元素。
从上图可以看出,即是对原本默认的pop和push都进行方向上的限定。甚至可以认为是栈和队列的结合体??
当然了我提到的这个只是皮毛,但是确实实用性是比较高的(个人感觉)。
详情的话建议官网哈,附上链接c++ deque。
教材中还有关于循环队列等的内容,我想着数据结构重要的不是知道有哪些(例如树,图,栈等),也不是学会了足够多足够好的算法。而是学会判断,判断哪种结构哪种算法真正能帮助解题!
这里直接做题好啦。
题意还是很明确的,暴力解法也是一目了然的。只需遍历原nums数组时对每个合法位置做窗口内的判断即可。代码很清晰,时间耗费也足够长
那么如何进行优化呢,既然是“队列”的题目,如何利用此数据结构呢?
先对暴力算法进行分析,其实时间的耗费就在于我们做了大量的重复遍历。
以例题为例,看其前4个数,即“1 3 -1 -3”,当我们求取出第一个窗口(前三个数)的最大值3后。我们在第二个窗口又对“3,-1”进行了判断,这就是不必要的操作了。当我们得知这个最大值3对第二个窗口仍然有效(即在第二个窗口内)时,实际仅需要对新增的那个-3进行判断就行了!
既然我们已经找到了弱点,那就可以进行optimization啦!
先想到对于这个滑动窗口,越先的数在后面的窗口实际是无效的,是需要被弹出的。因此满足的自然就是先进先出的一种规则,用到的就是队列了。
我们需要维护一个队列,它需要有什么特征?如下:
队列头需要是窗口的最大值。每次遍历到新元素的时候,需要进行两步操作。一是看当前队头元素是否有效,也就是是否在此元素的窗口内。如果无效则需要将此元素弹出。二就是将此元素和从队尾开始的元素不断进行比较,如果此元素大则将队尾的元素不断弹出,通过此方式找到此元素应在的合适位置。
再想上面的步骤,元素可以从队头队尾进行push和pop操作,自然就是deque了。
很多时候语言讲起来很费力,用debug的方式会更加浅显易懂。
首先代码如下所示:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> d; // 双端队列实现“单调队列” , 队列装的值都是元素下标
// 只有利用下标值才能更好地判断是否在滑动窗口这个区间内
vector<int> v; // 返回答案值的容器
for(int i = 0 ; i < int(nums.size()) ; i ++){
// 每次遍历到新值就要同队尾元素不断比较,如比队尾的大,就把队尾那个pop掉(队尾那个一定没有用了,因为我们永远找大的值,小的自然无用)。
while(!d.empty() && nums[i] > nums[d.back()])
d.pop_back();
// 查看当前队头的下标是否是有效的。试想当我们遍历到了位置下标3,队头还存储着下标0的话,即使下标0的值比下标3的值大也无法成为3那个窗口的最大值,因为不在范围内。(0, 1, 2)是一个窗口,(1,2,3)是一个窗口,因此0对3是无效的。
// 一旦是无效的,就把队头这个元素也pop掉
// 思考,为什么不用while?仅弹出一次就行。
if(!d.empty() && d.front() < i - k + 1)
d.pop_front();
// 将当前下标存储入队尾。
d.push_back(i);
// 不是一开始就记录值,从前k个数之后才能开始滑动,否则第一个区间是不完整的。
if(i >= k - 1){
v.push_back(nums[d.front()]);
}
}
return v;
}
来看debug的过程详解:(着重注意查看的是d的变化)
i = 0时,遍历到第一个元素,都为空,啥操作都干不了就直接装入此下标。
当 i = 1时,因为是3,大于1。那么1实际上已经是一个无效值了,不仅对当前窗口无效,对后面的窗口只会更加无效,会在第一个while循环中弹出,然后队列重新为空,然后装入下标1。
接下来就是i = 2,对应的值为-1,此值比3小。但不能光顾前不顾后,-1在这个窗口是不及3,但可能对于后面的窗口是有用的呀!!于是-1会被压入队尾,听候发落!
同时注意!!此时第一个窗口区间长度已经够啦,所以就直接从队头取出索引值,对应到nums中就是第一个最大值啦!!(v中会加入第一个数据啦)
接下来是i = 3,此时值为-3。还是上面那个解释原理,虽然其不仅比不过当前在队列中的-1或是3,但对于后面的队列可能是有用的!因此还是会将其压入队尾。
此时很重要的一点就是要看看当前的队头索引,即1,对现在的窗口是否还能够生效。可以发现,索引1刚好是第二个窗口的开头是有效的,因此会继续保留其队头的位置。同时返回容器v仍旧是装入这个值。
后面就不继续了,可以自己去调试。
想想代码里的思考题,为啥对队头是否有效只需判断一次??
其实我们仅将有可能对后续窗口有用的数进行了保存,且同一窗口内偏前的数一旦比偏后的数小的话都会直接被踢出。
所以如果出了假设情况的话,必然会违反第二条规则。因此只需弹出一次即可。
好了,接下来又是一道很有灵性的题目。长这样:
又是一道靠大神题解写的题。
官方题解就很朴素。其主要思想就是贪心算法。就是对每个时间点进行分析。用一对值(validtime, rest)来表示数据,意为:某一任务的下一个可行时间,某一任务剩下数量。我们会在一个可行时间点总是去寻找到目前剩余数量最大的那个任务。如没有任何可行时间与当前符合,就是待机状态。
这个想法用到哈希表会大大节省时间,这儿因为哈希会在后期复习到,这儿先不提。
俗话说得好,高手在民间,官方题解就是太官方了有constraints。
这儿可以直接先给一扇传送门桶思想解决任务调度问题。
图解非常详细,我就稍微做个归纳和最后的证明。
!!过程一定要配合图示来看!!
过程:
以最多的那个任务建桶,任务数量就是行数。第一列即填充任务号。接下来这些个任务号后面都得跟上冷却时间个数的空格。其他任务我们都以列的形式不断填充空格(冷却时间)。
这样一来基本流程都知道了,要做一些细致的分析。
我们断定,除了最后一行,其余的部分总面积是必然要用掉的时间。最后一行花费掉的时间仅仅是和这个最多任务数数量一样多的任务的个数,当然了本身这个1不能忘掉。(因为我们以列排,只有数量一样多时最后一个任务才会排在同一行)
最后我们取的是 总任务数和我们用刚刚算法求出值的较大的一个。
why? 其实很容易,我们用设计的算法求的实际上是只考虑了填充满冷却时间+数量最多的那些任务。我们不能确定是否有任务没被安排啊!!!
那如果确实有任务没被安排的话,实际上我们只要扩充原先的那个桶,将那些任务以列的方式不断添加于桶右端即可。那么这样一来,实际就是每个连续的时间都有在做任务,即总时间就是总任务数。
于是,最后的代码是这样的:
(!!一定一定要好好理解一下大神的思想,not me!!)
int leastInterval(vector<char>& tasks, int n) {
if(!n)
return tasks.size();
vector<int> z(26); // 用于统计某一个任务(都是大写字母,共26个)的数量
for (int i = 0; i < int(tasks.size()); i ++){
++z[tasks[i] - 'A']; // 遍历,对一项特定任务为其添加数量
}
sort(z.rbegin(), z.rend()); // 排序,递减排序
int cnt = 1; // 用于统计有多少个和当前最多的任务数量相同的 任务个数
while(cnt < z.size() && z[cnt] == z[0]) cnt++;
int time = cnt + (z[0] - 1) * (n + 1); // 必须要用的时间+和cnt含义
int result = max(time, int(tasks.size()));// 见上述证明
return result;
}
大神算法效率也很不戳:
难题真的本身生涩难懂加上没有思路,基本都会包含其他算法,不是特别友好吧。于是,咕咕咕!!!
大佬自行移步,传送门困难-队列。
(敬请关注后续的单调栈和优先队列哈)
到这里,队列和栈就结束了,这两个数据结构很形象基础操作也不难。
复习两者基础操作的可以看到这儿!
栈实现队列:
我的垃圾代码,都是以前写的了也懒得改进了,觉得没啥意义。
class CQueue {
public:
stack<int> s1;
stack<int> s2;
CQueue() {
}
void appendTail(int value) {
s1.push(value);
}
int deleteHead() {
if(s1.empty())
return -1;
while(s1.size() != 1){
s2.push(s1.top());
s1.pop();
}
int ans = s1.top();
s1.pop();
while(!s2.empty()){
s1.push(s2.top());
s2.pop();
}
return ans;
}
};
化栈为队:
class MyQueue {
public:
stack<int> s1;
stack<int> s2;
/** Initialize your data structure here. */
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
s1.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
while(!s1.empty()){
s2.push(s1.top());
s1.pop();
}
int ans = s2.top();
s2.pop();
while(!s2.empty()){
s1.push(s2.top());
s2.pop();
}
return ans;
}
/** Get the front element. */
int peek() {
while(!s1.empty()){
s2.push(s1.top());
s1.pop();
}
int ans = s2.top();
while(!s2.empty()){
s1.push(s2.top());
s2.pop();
}
return ans;
}
/** Returns whether the queue is empty. */
bool empty() {
if(s1.empty())
return true;
return false;
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
class MyStack {
public:
queue<int> q;
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
q.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
queue<int> temp;
while(q.size() > 1){
temp.push(q.front());
q.pop();
}
int ans = q.front();
q = temp;
return ans;
}
/** Get the top element. */
int top() {
queue<int> temp = q;
while(temp.size() > 1){
temp.pop();
}
return temp.front();
}
/** Returns whether the stack is empty. */
bool empty() {
return q.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/