代码随想录之栈与队列篇
T232-使用栈实现队列
见力扣第232题[使用栈实现队列]
题目描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)将元素 x 推到队列的末尾int pop()从队列的开头移除并返回元素int peek()返回队列开头的元素boolean empty()如果队列为空,返回true;否则,返回false
我的思路
- 使用两个栈来模拟队列
- 执行
push(int x)操作的时候,将元素压入到pushStack中 - 执行
pop()的时候,判断popStack中是否有元素,如果没有,将pushStack中的元素倒入popStack中
class MyQueue {
private Stack<Integer> pushStack;
private Stack<Integer> popStack;
public MyQueue() {
this.pushStack = new Stack<>();
this.popStack = new Stack<>();
}
public void push(int x) {
pushStack.push(x);
}
public int pop() {
// 先检查 popStack 中是否有元素
if (popStack.isEmpty()) {
transfer(pushStack, popStack);
}
// 从 popStack 中弹出元素
return popStack.pop();
}
public int peek() {
if (popStack.isEmpty()) {
transfer(pushStack, popStack);
}
return popStack.peek();
}
public boolean empty() {
return popStack.isEmpty() && pushStack.isEmpty();
}
private void transfer(Stack<Integer> pushStack, Stack<Integer> popStack) {
while (!pushStack.isEmpty()) {
popStack.push(pushStack.pop());
}
}
}
- 时间复杂度:
push()和empty()方法的时间复杂度为,pop()和peek()的平均时间复杂度为- 空间复杂度:。假设队列有次
push()的操作,队列中会有n个元素。
T225-使用队列实现栈
见力扣第225题[使用队列实现栈]
题目描述
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x)将元素 x 压入栈顶。int pop()移除并返回栈顶元素。int top()返回栈顶元素。boolean empty()如果栈是空的,返回true;否则,返回false。
我的思路
- 添加元素和删除元素都在队尾
class MyStack {
private final ArrayList<Integer> list;
private int sz;
public MyStack() {
list = new ArrayList<>();
sz = 0;
}
public void push(int x) {
list.add(x);
sz++;
}
public int pop() {
int val = list.get(sz - 1);
list.remove(sz - 1);
sz--;
return val;
}
public int top() {
return list.get(sz - 1);
}
public boolean empty() {
return sz == 0;
}
}
- 时间复杂度:,添加、删除以及对末尾元素的访问都是常数级的复杂度
- 空间复杂度:,需要使用
ArrayList存储元素
T20-有效的括号
见力扣第20题[有效的括号]
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
我的思路
- 使用栈来存储
s中的每个字符 - 如果
s.charAt(i)恰好和stack.peek()是一对- 将栈顶元素弹出:
stack.pop() - 将
s.charAt(i)压入栈中
- 将栈顶元素弹出:
- 判断
stack是否为空
public boolean isValid(String s) {
if (s.length() % 2 != 0) return false;
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (!stack.isEmpty() && (c - stack.peek()) <= 2 && (c - stack.peek()) > 0) {
stack.pop();
} else {
stack.push(c);
}
}
return stack.isEmpty();
}
- 时间复杂度:,遍历整个字符串
- 空间复杂度:,需要栈存储未匹配的括号
T1047-删除字符串中的所有相邻重复项
见力扣第1047题[删除字符串中的所有相邻重复项]
题目描述
给出由小写字母组成的字符串 s,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 s 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
我的思路
- 这一题和括号匹配问题解法相似,都是使用栈
- 如果匹配,则弹出栈顶元素,否则压入栈中
public String removeDuplicates(String s) {
if (s.length() <= 1) return s;
ArrayDeque<Character> q = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (!q.isEmpty() && c == q.peekLast()) {
q.removeLast();
} else {
q.addLast(c);
}
}
StringBuilder sb = new StringBuilder();
for (char c : q) {
sb.append(c);
}
return sb.toString();
}
- 时间复杂度:,遍历整个字符串
- 空间复杂度:,使用一个栈存储未被匹配的元素
T150-逆波兰表达式求值
见力扣第150题[逆波兰表达式求值]
题目描述
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'、'-'、'*'和'/'。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
我的思路
- 使用一个栈来存储操作数
- 遍历字符串的字符
- 如果字符是运算符,则弹出两个操作数,将结果入栈
- 如果字符是操作数,则压入栈中
- 返回操作数栈中的运算结果
public int evalRPN(String[] tokens) {
if (tokens.length <= 2) return Integer.parseInt(tokens[tokens.length - 1]);
int[] dataStack = new int[tokens.length / 2 + 1]; // 最多 n / 2 + 1 个操作数
int index = 0; // 模拟栈帧
for (String token : tokens) {
switch (token) {
case "+": {
// 加法,取出两位操作数相加,计算结果赋值到第一个操作数的位置
dataStack[index - 2] = dataStack[index - 2] + dataStack[index - 1];
// 栈帧指向有效操作数的后一位
index--;
break;
}
case "-": {
dataStack[index - 2] = dataStack[index - 2] - dataStack[index - 1];
index--;
break;
}
case "*": {
dataStack[index - 2] = dataStack[index - 2] * dataStack[index - 1];
index--;
break;
}
case "/": {
dataStack[index - 2] = dataStack[index - 2] / dataStack[index - 1];
index--;
break;
}
default: {
// 操作数,直接入栈
dataStack[index++] = Integer.parseInt(token);
}
}
}
return dataStack[0];
}
时间复杂度:,需要遍历一次
tokens数组空间复杂度:,需要长度为的数组模拟栈
T239-滑动窗口最大值
见力扣第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
我的思路
- 维护一个优先队列,存储窗口中的元素
- 窗口滑动的时候,从队列中增删指定的元素
- 然后将堆顶的元素添加到结果数组中
超时
思路
- 使用单调队列,队列中存储元素的索引,并且索引对应的元素时严格单调递减的
- 每次窗口滑动的时候
- 将即将入队的元素和队尾元素作比较,如果队尾元素小于即将入队的元素,则直接弹出队尾元素
- 如果队首的索引小于窗口的左边界,则弹出此索引,直到该队首所在的索引在窗口的范围之内
public int[] maxSlidingWindowI(int[] nums, int k) {
int n = nums.length;
Deque<Integer> dq = new ArrayDeque<>();
int[] res = new int[n - k + 1];
int l = 0;
int r = 0;
while (r < k) {
while (!dq.isEmpty() && nums[r] >= nums[dq.peekLast()]) {
dq.pollLast();
}
dq.offerLast(r++);
}
res[l] = nums[dq.peekFirst()];
while (r < n) {
// 如果队尾索引指向的元素小于或者等于准备的入队的元素,直接弹出
while (!dq.isEmpty() && nums[r] >= nums[dq.peekLast()]) {
dq.pollLast();
}
dq.offerLast(r++);
l++;
// 如果队首索引不在窗口中
while (dq.peekFirst() < l) {
dq.pollFirst();
}
res[l] = nums[dq.peekFirst()];
}
return res;
}
- 时间复杂度:,每个下标恰好被放入到队列中一次,并且最多被队列弹出一次
- 空间复杂度:,队列中最多不会存储超过窗口长度即个元素,因此队列使用的空间为
T347-前K个高频元素
见力扣第347题[前K个高频元素]
题目描述
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
我的思路
- 使用
map记录每个数的频率, - 遍历
map,将每个数的频率和数记录到List<int[]>中 - 调用集合的
Collections.sort()方法,重写Comparator<>接口 - 遍历前
k个频率比较高的元素,赋值给结果数组
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (!map.containsKey(num)) {
map.put(num, 0);
}
map.put(num, map.get(num) + 1);
}
List<int[]> frequency = new ArrayList<>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int[] pairs = new int[2];
pairs[0] = entry.getKey();
pairs[1] = entry.getValue();
frequency.add(pairs);
}
Collections.sort(frequency, (o1, o2) -> o2[1] - o1[1]);
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = frequency.get(i)[0];
}
return res;
}
优化方案
- 桶排序,记录每个数字出现的频率
public int[] topKFrequentI(int[] nums, int k) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
int[] ans = new int[k];
// 找到数组中的最大值和最小值
for (int i : nums) {
max = Math.max(i, max); // 更新最大值
min = Math.min(i, min); // 更新最小值
}
// 创建一个桶数组,用于记录每个数字出现的频率
int[] buckets = new int[max - min + 1];
for (int num : nums) {
buckets[num - min]++; // 将数字映射到桶数组中,并增加频率
}
// 找到桶数组中的最大频率
int maxF = 0;
for (int bucket : buckets) {
maxF = Math.max(bucket, maxF); // 更新最大频率
}
int index = 0; // 结果数组的索引
// 找到出现频率最高的 k 个数字
while (index < k) {
for (int i = 0; i < buckets.length; i++) {
if (maxF == buckets[i]) {
ans[index] = i + min; // 将数字从桶数组映射回原数组的值
index++; // 更新结果数组的索引
}
}
maxF--; // 减少最大频率,继续查找下一个频率最高的数字
}
return ans; // 返回结果数组
}
总结
-
用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。
-
接着,通过括号匹配问题、字符串去重问题、逆波兰表达式问题来系统讲解了栈在系统中的应用,以及使用技巧。
-
通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。