:::tips
作者:LemonCat🐱
时间:2023-8-4
:::
第五章 栈与队列part01
:::info
- 理论基础
- 232.用栈实现队列
- 用队列实现栈
:::
理论基础
:::tips 了解一下 栈与队列的内部实现机制,文中是以C++为例讲解的。
文章讲解:代码随想录 :::232.用栈实现队列
:::tips 先看视频,了解一下模拟的过程,然后写代码会轻松很多
题目链接:力扣 232.用栈实现队列
文章讲解/视频讲解:代码随想录 :::
- 用队列实现栈
:::
方法 - 双栈实现队列
- 使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的
- 需要两个栈一个输入栈,一个输出栈
下面动画模拟以下队列的执行过程:
queue.push(1);
queue.push(2);
queue.pop(); 注意此时的输出栈的操作
queue.push(3);
queue.push(4);
queue.pop();
queue.pop();注意此时的输出栈的操作
queue.pop();
queue.empty();
- 在push数据的时候,只要数据放进输入栈就好
- 在pop数据的时候,输出栈如果为空,就把进栈数据全部导入进来,再从出栈弹出数据
- 如果输出栈不为空,则直接从出栈弹出数据就可以了
- 在peek数据的时候,过程同 pop
- 我们可以将 两者相同的过程 -> 把进栈数据全部导入到出栈 整合成方法
dumpStackIn
- 我们可以将 两者相同的过程 -> 把进栈数据全部导入到出栈 整合成方法
- 判断队列为空 -> 如果进栈和出栈都为空的话,说明模拟的队列为空了
class MyQueue {
Stack<Integer> stackIn; // 入队的栈
Stack<Integer> stackOut; // 出队的栈
// 初始化
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
// 入队
public void push(int x) {
stackIn.push(x);
}
// pop 和 peek 执行前都执行去执行 dumpStackIn 让入队的栈里的的元素都放到出队的栈中
public int pop() {
dumpStackIn();
return stackOut.pop();
}
public int peek() {
dumpStackIn();
return stackOut.peek();
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
// 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
private void dumpStackIn(){
if (!stackOut.isEmpty()) return;
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
225. 用队列实现栈
:::tips
可以大家惯性思维,以为还要两个队列来模拟栈,其实只用一个队列就可以模拟栈了。
建议大家掌握一个队列的方法,更简单一些,可以先看视频讲解
题目链接:力扣 225. 用队列实现栈
文章讲解/视频讲解:代码随想录
:::
方法一 - 两个Queue
- 两个队列
queue1
是正经存放数据的队列 - 存放的顺序与栈出栈顺序一致queue2
临时队列 用于帮助queue1
调整顺序用的
- 入栈
- 在加入元素时先将 q1 中的元素依次出栈压入 q2
- 然后将新加入的元素压入 q1
- 再将 q2 中的元素依次出栈压入 q1
- 其余操作 调用队列的操作即可模拟 栈的操作
- 因为顺序已经做了调整 -> 满足出栈顺序! ```java /**
方法一 - 两个 Queue / class MyStack {
Queue
queue1; Queue queue2; public MyStack() {
queue1 = new LinkedList<>(); // queue1 作为主要的队列,其元素排列顺序和出栈顺序相同
queue2 = new LinkedList<>(); // queue2 仅作为临时放置
}
// 入栈 // 1. 在加入元素时先将q1中的元素依次出栈压入q2 // 2. 然后将新加入的元素压入q1 // 3. 再将q2中的元素依次出栈压入q1 public void push(int x) {
while (!queue1.isEmpty()) {
queue2.add(queue1.poll());
}
queue1.add(x);
while (!queue2.isEmpty()) {
queue1.add(queue2.poll());
}
}
// 出栈 public int pop() {
return queue1.poll();
}
// 查看栈顶 public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
- 时间复杂度: push为O(n),其他为O(1)
- 空间复杂度: O(n)
<a name="ENRYG"></a>
### 方法二 - 一个Queue
1. 入栈`push` -> 入队 `queue.add(x);`
2. 出栈 pop 和 返回栈顶 top - 两者都需要进行 环形操作 -> 将 size - 1 个元素 出栈再入栈
1. 出栈只需 再出队就好
2. 返回栈顶 需要保存出队的元素 `res` -> 然后将其入队 -> 返回那个 `res`
```java
/**
* 一个 Queue 实现
*/
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
// 入栈
public void push(int x) {
queue.add(x);
}
// 出栈
public int pop() {
rePosition();
return queue.poll();
}
// 查看栈顶
public int top() {
rePosition();
Integer res = queue.poll();
queue.add(res);
return res;
}
public boolean empty() {
return queue.isEmpty();
}
// 将 size - 1 个元素 出栈再入栈
private void rePosition() {
int size = queue.size();
size--;
while (size-- > 0) {
queue.add(queue.poll());
}
}
}
- 时间复杂度: push为O(n),其他为O(1)
-
第五章 栈与队列part02
:::info
- 有效的括号
- 删除字符串中的所有相邻重复项
- 逆波兰表达式求值
:::
20. 有效的括号
:::tips 栈的经典应用 思考有哪些不匹配的场景
题目链接:力扣 20. 有效的括号
文章讲解/视频讲解:代码随想录 :::方法 - 消消乐法
不匹配的情况
一共有三种情况 - 囊括了所有情形!
- 逆波兰表达式求值
:::
- 代码只要覆盖了上面三种不匹配的情况,就不会出问题
- 第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
- 第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
- 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
- 左括号和右括号全都匹配 - > 就是字符串遍历完之后,栈是空的,就说明全都匹配了
小技巧:
- 在匹配左括号的时候,右括号先入栈
- 这样就只需要比较当前元素和栈顶相不相等就可以了
- 比左括号先入栈代码实现要简单的多了
Code:
注意要 else if 否则判断
stack.isEmpty()
将出错/**
* 方法 - 消消乐法
*/
class Solution {
public boolean isValid(String s) {
// 若长度小于等于 1 直接返回
if (s.length() <= 1) {
return false;
}
Stack<Character> stack = new Stack<>();
char[] sChars = s.toCharArray();
for (char sChar : sChars) {
// 碰到左括号,就把相应的右括号入栈
if (sChar == '(') {
stack.push(')');
} else if (sChar == '[') {
stack.push(']');
} else if (sChar == '{') {
stack.push('}');
} else if (stack.isEmpty() || sChar != stack.peek()) {
return false;
} else { // 如果是右括号判断是否和栈顶元素匹配
stack.pop();
}
}
// 最后判断栈中元素是否相互匹配
return stack.isEmpty();
}
}
1047. 删除字符串中的所有相邻重复项
:::tips 栈的经典应用。
要知道栈为什么适合做这种类似于爱消除的操作,因为栈帮助我们记录了 遍历数组当前元素时候,前一个元素是什么。
题目链接:力扣 1047. 删除字符串中的所有相邻重复项
文章讲解/视频讲解:代码随想录 :::思路
本题也是用栈来解决的经典题目
- 烦人的地方在于 相同的字符串消除后 还有可能有新的相邻字符串
- 如果用 暴力的方法 就很不划算
- 如果用栈 就会眼前一亮
- 栈的就是存放遍历过的元素
- 当遍历当前的这个元素的时候 去栈里看一下我们是不是遍历过相同数值的相邻元素
-
从栈中弹出剩余元素,此时是字符串ac
-
方法一 - 消消乐法
class Solution {
public String removeDuplicates(String s) {
if (s.length() <= 1) {
return s;
}
Stack<Character> stack = new Stack<>();
// 当遍历当前的这个元素的时候 去栈里看一下我们是不是遍历过相同数值的相邻元素
// 若相同则直接将元素出栈 否则入栈
for (int i = 0; i < s.length(); i++) {
if (stack.isEmpty() || s.charAt(i) != stack.peek()) {
stack.push(s.charAt(i));
} else {
stack.pop();
}
}
// 剩余的元素即为不重复的元素
s = "";
while (!stack.isEmpty()) {
s = stack.pop() + s; // 由于栈是先进后出 所以 s 要拼接在字符串后面!!!
}
return s;
}
}
-
- 时间复杂度: O(n)
-
方法二 - 消消乐法 - 优化
拿字符串直接作为栈,省去了栈还要转为字符串的操作
class Solution {
public String removeDuplicates(String s) {
if (s.length() <= 1) {
return s;
}
StringBuilder sb = new StringBuilder(); // 把他当做栈
int index = -1;// 用于标记 sb 的长度 初始长度为 -1
for (int i = 0; i < s.length(); i++) {
if (index == -1 || s.charAt(i) != sb.charAt(index)) {
sb.append(s.charAt(i));
index++;
} else {
sb.deleteCharAt(index--);
}
}
return new String(sb);
}
}
时间复杂度: O(n)
- 空间复杂度: O(1),返回值不计空间复杂度
方法三 - 双指针
```java /**
方法三 - 双指针 */ class Solution { public String removeDuplicates(String s) {
if (s.length() == 1) {
return s;
}
char[] ch = s.toCharArray();
int fast = 0;
int slow = 0;
while (fast < ch.length) {
// 直接用fast指针覆盖slow指针的值
ch[slow] = ch[fast];
// 遇到前后相同值的就跳过,即slow指针后退一步,下次循环就可以直接被覆盖掉了
if (slow > 0 && ch[slow] == ch[slow - 1]) {
slow--;
} else {
slow++;
}
fast++;
}
return new String(ch, 0, slow);
150. 逆波兰表达式求值
:::tips 本题不难,但第一次做的话,会很难想到,所以先看视频,了解思路再去做题
题目链接:力扣 150. 逆波兰表达式求值
文章讲解/视频讲解:代码随想录 :::
思路
逆波兰表达式:
- 相当于是二叉树中的后序遍历
- 把运算符作为中间节点,按照后序遍历的规则画出一个二叉树
- 后序遍历 即树的 前后中遍历
-
使用后序遍历原因
我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。
- 例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算符,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法 十分麻不麻烦!
将中缀表达式,转化为后缀表达式之后:[“4”, “13”, “5”, “/“, “+”] ,就不一样了,计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的
方法 - 栈的妙用
注意 “-“ 和 “/“ 需要特殊处理 因为需要注意顺序!
米奇妙妙屋
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
Arrays.stream(tokens).forEach(t -> {
// 注意 - 和/ 需要特殊处理
switch (t) {
case "+": {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 + temp1);
break;
}
case "-": {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 - temp1);
break;
}
case "*": {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 * temp1);
break;
}
case "/": {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
break;
}
default:
stack.push(Integer.valueOf(t));
break;
}
});
return stack.pop();
}
}
-
第五章 栈与队列part03
:::tips
- 滑动窗口最大值
- 347.前 K 个高频元素
- 总结
:::
239. 滑动窗口最大值(难)
:::danger 队列的应用了 本题难,需要自己去构造单调队列
题目链接:力扣 239. 滑动窗口最大值
文章讲解/视频讲解:代码随想录 :::
方法一 - 自定义队列
- 暴力方法:遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法
- n 是数组的长度 k 是滑块的长度
- 自定义单调队列:
- 调队列维护队列里的元素方式
- 设计单调队列
pop
和push
操作要保持如下规则:pop(value)
:如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作push(value)
:如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止- 保持如上规则,每次窗口移动的时候
- 只要
queue.peek()
获取栈顶元素 - 就可以返回当前窗口的最大值
- 只要
- 用
Deque<Integer> queue = new LinkedList<>();
数据结构来实现- 示例:输入:
nums = [1,3,-1,-3,5,3,6,7]
和k = 3
- 示例:输入:
- 最后使用这个自定义单调队列即好
Code:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 长度为一 提前返回
if(nums.length == 1){
return nums;
}
MyQueue myQueue = new MyQueue(); // 自定义队列
int[] resArr = new int[nums.length - k + 1]; //存放结果元素的数组
// 先将 k 个元素放入队列
for (int i = 0; i < k; i++) {
myQueue.add(nums[i]);
}
// 开始滑动窗口
for (int i = k; i < nums.length; i++) {
resArr[i - k] = myQueue.peek(); // 获取最大值
myQueue.poll(nums[i - k]); // 移出尾部元素
myQueue.add(nums[i]); // 添加新元素
}
resArr[resArr.length - 1] = myQueue.peek(); // 获取最后一次的最大值
return resArr;
}
}
/**
* 自定义 队列
*/
class MyQueue {
Deque<Integer> queue = new LinkedList<>();
// 获取队顶元素 - 永远是最大值
public Integer peek() {
return queue.peek();
}
// 添加元素
// 1. 如果要添加的元素大于入口处的元素,就将入口元素弹出
// 2. 保证队列元素单调递减
// 比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2
public void add(Integer val) {
while (!queue.isEmpty() && val > queue.getLast()) {
queue.removeLast();
}
queue.add(val);
}
// 弹出元素
// 1. 判断队列当前是否为空
// 比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
public void poll(Integer val) {
if (!queue.isEmpty() && val.equals(queue.peek())) {
queue.poll();
}
}
}
- 时间复杂度: O(n)
-
方法二 - 双端队列手动实现单调队列
@TODO 方法二只简单看了一遍 有时间在研究第二种方法~
/**
* 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
* 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标)
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
ArrayDeque<Integer> deque = new ArrayDeque<>();
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
for(int i = 0; i < n; i++) {
// 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
// 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
while(!deque.isEmpty() && deque.peek() < i - k + 1){
deque.poll();
}
// 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
if(i >= k - 1){
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}
347.前 K 个高频元素(中等)
大/小顶堆的应用 在C++中就是优先级队列 大数据中取前k值 的经典思路 题目链接:力扣 347.前 K 个高频元素 文章讲解/视频讲解:代码随想录
方法一 - 小顶堆
- 统计元素出现的频率 -> 使用map来进行统计
- 对频率排序 -> 容器适配器就是优先级队列 =>
PriorityQueue
- 优先级队列 -> 一个披着队列外衣的堆
- 堆 -> 是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
- 大顶堆: 头部比孩子大 根节点是最大的元素
- 小顶堆: 根节点是最小的元素,父亲比左右子要小
- 找出前K个高频元素
- 建议用小顶堆,因为要统计最大前k个元素
- 只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
Code:
/**
* 方法一 - 小顶堆
*/
class Solution3 {
public int[] topKFrequent(int[] nums, int k) {
// Map 来存放统计每个数出现的频率
// K:数值
// V:频率
Map<Integer, Integer> map = new HashMap<>();
Arrays.stream(nums).forEach(num -> {
map.put(num, map.getOrDefault(num, 0) + 1);
});
// 在优先队列PriorityQueue 中存储 二元组(num,cnt)
// 1. cnt 表示元素值
// 2. num 在数组中的出现次数
// 出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (queue.size() < k) { //小顶堆只需要维持k个元素有序
queue.add(new int[]{entry.getKey(), entry.getValue()});
}
// 当前元素出现次数大于小顶堆的根结点 (当前这k个元素中出现次数最少的那个)
else if (entry.getValue() > queue.peek()[1]) {
queue.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
queue.add(new int[]{entry.getKey(), entry.getValue()});
}
}
// 小顶堆需要反向遍历
// 依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
int[] ans = new int[k];
for (int i = k - 1; i >= 0; i--) {
ans[i] = queue.poll()[0];
}
return ans;
}
}
总结
:::tips 栈与队列总结:代码随想录 :::
- 感觉不容易 需要复习了~ QAQ