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
操作是合法的。
示例:
MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek(); // 返回 1
queue.pop(); // 返回 1
queue.empty(); // 返回 false
解题思路:
(栈,队列) O(n)
这道题目与 LeetCode 225. Implement Stack using Queues 类似。
我们用一个栈来存储队列中的元素,另外还需要一个辅助栈,用来辅助实现 pop()
和 peek()
操作。
四种操作的实现方式如下:
push(x)
– 直接将x
插入栈顶;
pop()
– 即需要弹出栈底元素,我们先将栈底以上的所有元素插入辅助栈中,然后弹出栈底元素,最后再将辅助栈中的元素重新压入当前栈中;
peek()
– 返回栈顶元素,同理,我们先将栈底以上的所有元素插入辅助栈中,然后输出栈底元素,最后再将辅助栈中的元素重新压入当前栈中,恢复当前栈原状;
empty()
– 返回当前栈是否为空;
时间复杂度分析:push(x)
和 emtpy()
均只有一次操作,时间复杂度是 O(1)
;pop()
和 peek()
涉及到 n
次操作,所以时间复杂度是 O(n)
。
C++代码:
class MyQueue {
public:
stack<int> sta, cache;
MyQueue() {
}
void push(int x) {
sta.push(x);
}
int pop() {
while (!sta.empty()) cache.push(sta.top()), sta.pop();
int x = cache.top();
cache.pop();
while (!cache.empty()) sta.push(cache.top()), cache.pop();
return x;
}
int peek() {
while (!sta.empty()) cache.push(sta.top()), sta.pop();
int x = cache.top();
while (!cache.empty()) sta.push(cache.top()), cache.pop();
return x;
}
bool empty() {
return sta.empty();
}
};
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
这些操作。
解题思路:
(队列,栈) O(n)
我们用一个队列来存储栈中元素。对于栈中的四种操作:
push(x)
– 直接入队;pop()
– 即需要弹出队尾元素。我们先将队首元素弹出并插入队尾,循环n−1
次,n
是队列长度。此时队尾元素已经在队首了,直接将其弹出即可;top()
– 即返回队尾元素。同理,我们先将队首元素弹出并插入队尾,循环n−1
次,n
是队列长度。此时队尾元素已经在队首了,直接将其返回。不要忘记将其弹出并插入队尾,恢复队列原状;empty()
– 返回队列是否为空;
时间复杂度分析:push()
和 empty()
均只有一次操作,时间复杂度是 O(1)
,pop()
和 top()
需要循环 n
次,所以时间复杂度是 O(n)
。
C++ 代码:
class MyStack {
public:
queue<int> que;
MyStack() {
}
void push(int x) {
que.push(x);
}
int pop() {
int cnt = que.size() - 1;
while (cnt--) que.push(que.front()), que.pop();
int x = que.front();
que.pop();
return x;
}
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 仅由括号 '()[]{}'
组成
解题思路:
(栈) O(n)
从前往后枚举每个字符
- 当遇到左括号,则将元素压进栈中
- 当遇到右括号时
- 如果栈为空,说明此时已没有左括号与其相匹配,
return false
; - 如果栈顶的左括号与其右括号匹配,则将栈顶元素
pop
出即可; - 否则,说明栈顶的左括号与其不匹配,
return false
。
- 遍历完所有的括号后,若栈为空,则说明所有字符都已经匹配好了;若栈不为空,则说明栈中还存在未能匹配的左括号。
注意⚠️:由于 ‘{’ 和 ‘}’ 以及 ‘(’ 和 ‘)’ 他们的字符数值只相差1,而 ‘[’ 和 ‘]’ 的字符数值只相差2,因此还可以通过这个特性简化代码,代码在最下方
C++代码:
方法一
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for (auto c : s) {
if (c == '(' || c == '[' || c == '{') stk.push(c);
else {
if (stk.empty()) return false;
if (stk.top() == '(' && c == ')') stk.pop();
else if (stk.top() == '[' && c == ']') stk.pop();
else if (stk.top() == '{' && c == '}') stk.pop();
else return false;
}
}
return stk.empty();
}
};
方法二
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for (auto c : s) {
if (c == '(' || c == '[' || c == '{') stk.push(c);
else {
if (!stk.empty() && abs(stk.top() - c) <= 2) stk.pop();
else return false;
}
}
return stk.empty();
}
};
1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S
上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,
这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",
其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= S.length <= 20000
S
仅由小写英文字母组成。
解题思路:
可以运用栈和队列 遍历字符串中的每一个字母,当栈顶字母和当前字母不相同时,将当前元素入栈;否则,当前元素和栈顶元素相同,且栈顶字母和当前字母相邻,此时就将栈顶元素出栈。运用队列也是一个道理。
C++代码:
方法一
class Solution {
public:
string removeDuplicates(string s) {
string res;
for (auto c: s) {
if (res.size() && res.back() == c) res.pop_back();
else res += c;
}
return res;
}
};
方法二
class Solution {
public:
string removeDuplicates(string s) {
stack<char> res;
for (auto c: s) {
if (!res.empty() && res.top() == c) res.pop();
else res.push(c);
}
string r;
while (res.size()) {
r += res.top();
res.pop();
}
reverse(r.begin(), r.end());
return r;
}
};
150. 逆波兰表达式求值
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
提示:
- 1 <=
tokens.length
<= 104 tokens[i]
是一个算符("+"
、"-"
、"*"
或"/"
),或是在范围[-200, 200]
内的一个整数
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。 - 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。 - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
解题思路:
(栈操作) O(n)
遍历所有元素。如果当前元素是整数,则压入栈;如果是运算符,则将栈顶两个元素弹出做相应运算,再将结果入栈。
最终表达式扫描完后,栈里的数就是结果。
时间复杂度分析:每个元素仅被遍历一次,且每次遍历时仅涉及常数次操作,所以时间复杂度是 O(n)
。
C++ 代码:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> sta;
for (auto &t : tokens) {
if (t == "+" || t == "-" || t == "*" || t == "/") {
int a = sta.top();
sta.pop();
int b = sta.top();
sta.pop();
if (t == "+") sta.push(a + b);
else if (t == "-") sta.push(b - a);
else if (t == "*") sta.push(a * b);
else sta.push(b / a);
}
else sta.push(stoi(t));
}
return sta.top();
}
};
239. 滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
- 1 <=
nums.length
<= 105 - -104 <=
nums[i]
<= 104 - 1 <=
k
<=nums.length
解题思路:
滑动窗口最大值属于单调队列题型,运用for
循环做以下四步:
- 解决队头已经弹出窗口的问题;
- 解决队尾与当前元素
nums[i]
不满足单调性的问题; - 将当前元素下标加入队尾;
- 如果满足条件则输出结果;
注意⚠️:
- 上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
- 队列中存的是原数组的下标,取值时要再套一层,
nums[q[]]
;
C++代码:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q; // q维护的是一个单调递减双端队列的下标
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
if (q.size() && i - k + 1 > q.front()) q.pop_front(); // 如果队头元素超出了队列所能维护的范围,将队头弹出
while (q.size() && nums[i] >= nums[q.back()]) q.pop_back(); //如果新加入的元素比队尾所对应的元素值大,那么将队尾弹出
q.push_back(i); //将新的元素下标插入到队尾
if (i >= k - 1) res.push_back(nums[q.front()]); // 如果队列中的元素总数达到了k个,就将队头所对应的元素加入到结果中
}
return res;
}
};
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
是数组大小。
解题思路一:
(哈希表,计数排序) O(n)
首先用 哈希表 统计出所有数出现的次数。
由于所有数出现的次数都在 1 到 n 之间,所以我们可以用 计数排序 的思想,统计出次数最多的前 k
个元素的下界。然后将所有出现次数大于等于下界的数输出。
时间复杂度分析:用哈希表统计每个数出现次数的计算量是 O(n)
,计数排序的计算量是 O(n)
,最终用下界过滤结果的计算量也是 O(n)
,所以总时间复杂度是 O(n)
。
C++ 代码:
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt;
vector<int> res;
for (int x : nums) cnt[x] ++;
int n = nums.size();
vector<int>s(n + 1, 0); //用s数组进行计数排序
for (auto &p : cnt) s[p.second] ++ ;
int i = n, t = 0;
while (t < k) t += s[i -- ]; //通过t来统计前k个高频元素,分界线为i和i+1
for (auto &p : hash)
if (p.second > i)
res.push_back(p.first);
return res;
}
};
解题思路二:
首先用 哈希表 统计出所有数出现的次数。
然后将每个元素按照出现的次数进行降序排序,将出现前k
个元素作为结果返回。
class Solution {
public:
static bool comp(const pair<int,int> &a, const pair<int,int> &b) {
return a.second > b.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt; //统计每个元素出现的次数
for (auto c : nums) cnt[c]++;
vector<pair<int,int>> b(cnt.begin(), cnt.end());
sort(b.begin(),b.end(),comp);
vector<int> res;
for (int i = 0; i < k; i++) res.push_back(b[i].first);
return res;
}
};