一、栈与队列的实现
232. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
示例 1:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、peek
和empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
进阶:
- 你能否实现每个操作均摊时间复杂度为
O(1)
的队列?换句话说,执行n
个操作的总时间复杂度为O(n)
,即使其中一个操作可能花费较长时间。
题解
java
- 两个栈进行倒数据,关键是把握好倒数据的时机
class MyQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
int size;
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
this.size = 0;
}
public void push(int x) {
stack1.push(x);
size++;
}
public int pop() {
if(stack2.isEmpty()) {
while(!stack1.isEmpty()) stack2.push(stack1.pop());
}
size--;
return stack2.pop();
}
public int peek() {
if(stack2.isEmpty()) {
while(!stack1.isEmpty()) stack2.push(stack1.pop());
}
return stack2.peek();
}
public boolean empty() {
return size == 0;
}
}
C++
-
注意:
1.c++的成员变量size要初始化
2.
pop()
函数没有返回值3.
top()
函数相当于peek()
函数 -
对于逻辑类似的函数,可以复用之前实现的函数,这是个好习惯,例如
peek()
复用pop()
,做些修改
class MyQueue {
public:
stack<int> stIn; stack<int> stOut;
int size;// 成员变量初始值无法预料,需要在构造函数中初始化
MyQueue() {
this->size = 0; // 要初始化,否则成员变量初始值无法预料
}
void push(int x) {
stIn.push(x);
size++;
}
int pop() {
if(stOut.empty()) {
while(!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop(); // 注意,c++的pop()没有返回值
}
}
size--;
int res = stOut.top(); // c++的top(),即java的peek()
stOut.pop();
return res;
}
int peek() {
// 可以复用pop函数
int res = pop();
stOut.push(res); size++; // 要维护原栈数据和size
return res;
}
bool empty() {
return size == 0;
}
};
225. 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
注意:
- 你只能使用队列的基本操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、top
和empty
- 每次调用
pop
和top
都保证栈不为空
**进阶:**你能否仅用一个队列来实现栈
题解
题解1:两个队列
- 用两个队列,并不是来回倒数据,因为数据顺序不会发生变化
- 另一个队列只是用来备份,
- 因为队列只能末端插入,首端弹出,即先进先出,不能从末端弹出
- 而栈是先进先出,栈顶元素是队列最后一个进入的,因此,需要将前面元素都弹出才能拿到
- 因此,用另一个队列容纳剔除的其余元素,留下的就是要弹出的末端元素
- 但是c++队列可以查询首尾元素,因此,可以直接调用实现好的
pop()
函数或back()
函数
class MyStack {
public:
queue<int> queue1;
queue<int> help;
MyStack() {}
void push(int x) {
queue1.push(x);
}
// 删除、要将数据导入help栈中,留下最后一个栈顶元素拿出
int pop() {
// 1.对于删除元素操作,一定要先判断边界
if(queue1.empty()) return -1;
// 2.倒数据
while(queue1.size() > 1) {
help.push(queue1.front());
queue1.pop(); // 无返回类型
}
// 3.找到要删除的数据,并将两个队列对调
int res = queue1.front();
queue1.pop();
swap(queue1, help);
return res;
}
int top() {
return queue1.back();
}
bool empty() {
return queue1.empty();
}
};
题解2:使用一个队列
- **一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) **
- 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了
class MyStack {
public:
queue<int> que;
MyStack() {}
void push(int x) {
que.push(x);
}
// 删除、要将数据导入help栈中,留下最后一个栈顶元素拿出
int pop() {
// 1.对于删除元素操作,一定要先判断边界
if(que.empty()) return -1;
// 2.倒数据,将非最后一个元素依次拿出,再添加到队列尾部
int size = que.size();
while(size-- > 1) {
que.push(que.front());
que.pop(); // 无返回类型
}
// 3.找到要删除的数据,并真正弹出
int res = que.front();
que.pop();
return res;
}
int top() {
return que.back();
}
bool empty() {
return que.empty();
}
};
二、栈的应用
20. 有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
题解
题解1:将左括号入栈
- 问题的难点是,可能先列出若干个左括号,再列出一些右括号,要能记录最后一个左括号与最新右括号是否匹配
- 因此,此类问题,用栈解决最好,栈可以记录,栈也可以后进先出,每次拿到最新
- 因此,用一个栈记录之前遍历过的左括号,遇到右括号,匹配就相消,最后看是否消完即可
class Solution {
public:
stack<char> sta;
bool isValid(string s) {
for(char c : s) {
// 1.遇到左括号,就入栈
if(c == '(' || c == '[' || c == '{') sta.push(c);
// 2.遇到右括号,就弹出匹配
else {
if(sta.empty()) return false; // 有右括号却没有左括号
if(c == ')') {
if(sta.top() != '(') return false;
sta.pop();
} else if(c == ']') {
if(sta.top() != '[') return false;
sta.pop();
} else {
if(sta.top() != '{') return false;
sta.pop();
}
}
}
return sta.empty();
}
};
优化:将右括号入栈
-
在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,
-
比左括号先入栈代码实现要简单的多了!
class Solution {
public:
stack<char> sta;
bool isValid(string s) {
if(s.size() % 2 != 0) return false;
for(char c : s) {
// 1.遇到左括号,将对应右括号入栈
if(c == '(') sta.push(')');
else if(c == '[') sta.push(']');
else if(c == '{') sta.push('}');
// 2.遇到右括号,就弹出匹配
else if(sta.empty() || sta.top() != c) return false;
else sta.pop();
}
return sta.empty();
}
};
1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= S.length <= 20000
S
仅由小写英文字母组成。
题解
使用容器栈
- 同样是相消问题,可以使用栈
- 明确:
- 栈中放什么:与栈顶元素不同的元素
- 栈何时弹出:与栈顶元素相同时,将栈顶元素弹出
- 最后栈中留下的字符就是目标字符,组成字符串即可
class Solution {
public:
string removeDuplicates(string s) {
stack<char> sta;
for(char c : s) {
// 1.边界要先处理,top(),pop()前提是栈非空
if(sta.empty()) {
sta.push(c);
continue;
}
if(sta.top() != c) sta.push(c);
else sta.pop();
}
// 2.将栈中元素取出拼接成字符串
string res = "";
while(!sta.empty()) {
res += sta.top(); sta.pop();
}
reverse(res.begin(), res.end()); // 跳转顺序
return res;
}
};
将字符串作为栈使用
class Solution {
public:
string removeDuplicates(string s) {
string res;
for(char c : s) {
if(!res.empty() && res.back() == c) res.pop_back();
else res.push_back(c);
}
return res;
}
};
150. 逆波兰表达式求值
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的乘法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
提示:
1 <= tokens.length <= 104
tokens[i]
是一个算符("+"
、"-"
、"*"
或"/"
),或是在范围[-200, 200]
内的一个整数
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。 - 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。 - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
题解
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st; // 栈中储存的是运算中间结果
for(string str : tokens) {
// 1.为判断简便,先处理非数子符号
if(str == "+" || str == "-" || str == "*" || str == "/") {
int num1 = st.top(); st.pop(); // 每次要先弹出,再找下个元素
int num2 = st.top(); st.pop();
st.push(operate(num1, num2, str));
} else st.push(stoi(str));
}
return st.top();
}
private:
// 注意除法和减法:13 5 / 是13/5,故分子分母顺寻要调整
int operate(int num1, int num2, string& s) {
if(s == "+") return num2 + num1;
if(s == "-") return num2 - num1;
if(s == "*") return num2 * num1;
if(s == "/") return num2 / num1;
return -1;
}
};
三、单调队列
239. 滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
题解
-
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里的元素数值是由大到小的
-
删除冗余元素,维护单调队列,如果值小,且在队列前面,生命周期比后面元素短,定不会对结果有影响,这样的数就要删除,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHD7vdUk-1674957222439)(assets/e3251dacbb6f43baa26b107a04de4dd1.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wh6mn36r-1674957222440)(assets/06e5da7ad4d549ffb3e47f6e0310283c.png)]
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
myQueue myque;
vector<int> ans;
// 1.先形成k个元素的窗口,并维护成单调队列
for(int i = 0; i < k; i++) myque.push_value(nums[i]);
ans.push_back(myque.front());//形成窗口后再取结果,因此是i不是候选项
// 2.移动窗口,i为滑动窗口右边界,左边界每次减一
for(int i = k; i < nums.size(); i++) {
myque.pop_value(nums[i - k]);
myque.push_value(nums[i]);
ans.push_back(myque.front());
}
return ans;
}
private:
// 定义单调队列:三部曲
class myQueue {
public:
deque<int> que; // 使用双端队列实现的deque实现单调队列
// 1.剔除过期的队头(不是维护单调性,而是队首元素过期了,需要弹出)
// 删除指定过期元素,要判断是否为队头
// 具体如何定性为过期,是上层逻辑的事
void pop_value(int value) {
if(!que.empty() && value == que.front()) que.pop_front();
}
// 2.维护单调性,即添加元素时要先判断,删除冗余元素,维护单调
// 具体单调性如何定性,也是上层逻辑
void push_value(int value) {
while(!que.empty() && value > que.back()) que.pop_back();
que.push_back(value);
}
// 3.寻找队首的目标值更新答案
// 1和3的先后顺序取决于上层逻辑,先找答案还是先处理-i是否为候选者
int front() {return que.front();}
};
};
四、优先队列
347. 前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105
k
的取值范围是[1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的
**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n)
,其中 n
是数组大小。
题解
- 要统计元素出现频率
- 对频率排序
- 找出前K个高频元素
- 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的
- 要用小顶堆,因为要统计最大前k个元素,
- 即,维护k个元素的容器,保证这k个元素最大,每次有新的元素进入,弹出最小的一个
- 即,保留的元素是队列维护的,保证
topK
,元素大于k
时,就要使用小根堆排除这些元素中最小的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g3nimnVe-1674957222441)(assets/image-20221231112404986.png)]
确实 例如我们在写快排的cmp函数的时候,return left>right
就是从大到小,return left<right
就是从小到大。
优先级队列的定义正好反过来了,可能和优先级队列的源码实现有关(我没有仔细研究),我估计是底层实现上优先队列队首指向后面,队尾指向最前面的缘故!
(220条消息) c++之greater和less在stl中运用_海马有力量的博客-优快云博客
class Solution {
public:
// 1.重写比较器
struct mycompare {
bool operator() (const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 1.使用map统计每个元素出现的次数
unordered_map<int, int> map;
for(int val : nums) map[val]++;
// 2.使用小根堆,对map中的数据过滤,弹出小的数,保留前k个大的数
priority_queue<pair<int, int>, vector<pair<int, int>>, mycompare> q;
for(auto& a : map) {
// 入队,自动排成小根堆
q.push(a);
// 元素超过了k,就将最小的元素弹出
if(q.size() > k) q.pop();
}
// 3.将保留下来的k个元素的小根堆转为数组
vector<int> ans;
while(!q.empty()) {
ans.emplace_back(q.top().first); // emplace_back直接安放对象
q.pop();
}
return ans;
}
};