{{day10}}
栈(Stack)和队列(Queue)都是Java中常用的数据结构,它们在处理数据时遵循不同的原则。
1. 栈 (Stack)
栈是一种遵循后进先出(LIFO, Last-In-First-Out)原则的数据结构。可以将栈想象成一叠盘子,你只能从顶部添加或移除盘子。
Java中的栈主要特点:
- 插入操作称为"压栈"(push)
- 删除操作称为"弹栈"(pop)
- 只能访问栈顶元素
- Java提供了Stack类,它扩展自Vector类
栈的基本操作:
Stack<Integer> stack = new Stack<>();
// 压栈
stack.push(1);
stack.push(2);
stack.push(3);
// 弹栈
int top = stack.pop(); // 返回3
// 查看栈顶元素但不移除
int peek = stack.peek(); // 返回2
// 检查栈是否为空
boolean isEmpty = stack.empty();
栈的应用场景:
- 函数调用的实现
- 表达式求值
- 深度优先搜索算法
- 回溯算法
2. 队列 (Queue)
队列遵循先进先出(FIFO, First-In-First-Out)原则。可以将队列想象成排队买票,先到的人先买票离开。
Java中的队列主要特点:
- 插入操作发生在队尾,称为"入队"(enqueue)
- 删除操作发生在队首,称为"出队"(dequeue)
- Java提供了Queue接口,常用实现类有LinkedList和PriorityQueue
队列的基本操作:
Queue<String> queue = new LinkedList<>();
// 入队
queue.offer("A");
queue.offer("B");
queue.offer("C");
// 出队
String first = queue.poll(); // 返回"A"
// 查看队首元素但不移除
String peek = queue.peek(); // 返回"B"
// 检查队列是否为空
boolean isEmpty = queue.isEmpty();
队列的应用场景:
- 广度优先搜索算法
- 缓冲区实现
- 任务调度
- 消息队列系统
题目信息232. 用栈实现队列
- 题目链接: https://leetcode.cn/problems/implement-queue-using-stacks/
- 题目描述:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(
push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
解题思路
在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
代码实现
import org.junit.Test;
import java.util.Stack;
public class Day10 {
@Test
public void test1(){
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
System.out.println(myQueue.peek()); // return 1
System.out.println(myQueue.pop()); // return 1, queue is [2]
System.out.println(myQueue.empty()); // return false
}
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
dumpstackIn();
return stackOut.pop();
}
public int peek() {
dumpstackIn();
return stackOut.peek();
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
public void dumpstackIn(){
if (!stackOut.isEmpty()) return;
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
}
题目信息 225. 用队列实现栈
- 题目链接: https://leetcode.cn/problems/implement-stack-using-queues/description/
- 题目描述: 请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(
push
、top
、pop
和empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
解法一: {{使用两个 Queue 实现}}
解题思路
![[225.用队列实现栈.gif]]
代码实现
@Test
public void test2(){
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
System.out.println(myStack.top()); // 返回 2 System.out.println(myStack.pop()); // 返回 2 System.out.println(myStack.empty()); // 返回 False
}
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
Queue<Integer> queueTemp;
queueTemp = queue1;
queue1 = queue2;
queue2 = queueTemp;
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
解法二: {{用一个队列}}
解题思路
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。
代码实现
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();
int result = queue.poll();
queue.add(result);
return result;
}
public boolean empty() {
return queue.isEmpty();
}
public void rePosition(){
int size = queue.size();
size--;
while(size-->0)
queue.add(queue.poll());
}
}
题目信息 20. 有效的括号
- 题目链接: https://leetcode.cn/problems/valid-parentheses/
- 题目描述: 给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
注意
- s.length()
- 使用场景:当
s
是 String 类型(字符串)时。 - 解释:
length()
是 String 类的一个方法。 - 示例:
- 使用场景:当
String s = “Hello”;
int length = s.length(); // 返回 5
- 注意:这是一个方法调用,所以需要括号。
- s.length
- 使用场景:当 `s` 是数组类型时。
- 解释:`length` 是数组的一个属性(不是方法)。
- 示例:
```java
int[] s = {1, 2, 3, 4, 5};
int length = s.length; // 返回 5
- 注意:这是一个属性访问,不需要括号。
- 区别要点:
- 方法 vs 属性:
length()
是方法,length
是属性。 - 括号:方法调用需要括号,属性访问不需要。
- 适用类型:
length()
用于 String,length
用于数组。
- 方法 vs 属性:
解题思路
先来分析一下 这里有三种不匹配的情况,
-
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
-
第二种情况,括号没有多余,但是 括号的类型没有匹配上。
-
第三种情况,字符串里右方向的括号多余了,所以不匹配。
我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。
动画如下:
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。
分析完之后,代码其实就比较好写了,
但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
代码实现
public boolean isValid(String s) {
Deque<Character> deque = new LinkedList<>();
char ch;
for (int i = 0; i < s.length();i++){
ch = s.charAt(i);
if (ch == '('){
deque.push(')');
} else if (ch == '[') {
deque.push(']');
} else if (ch == '{') {
deque.push('}');
} else if (deque.isEmpty() || deque.peek() != ch) {
return false;
}else {
deque.pop();
}
}return deque.isEmpty();
}
题目信息 1047. 删除字符串中的所有相邻重复项
-
题目链接: https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/
-
题目描述: 给出由小写字母组成的字符串
S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
解法一: {{双指针}}
解题思路
- 字符串转换:
- 首先,我们将输入的字符串
s
转换为字符数组chars
。这样做是为了能够直接修改字符,提高效率。
- 首先,我们将输入的字符串
- 双指针设置:
- 我们使用两个指针:
slow
和fast
。 slow
指针:指向当前我们认为有效的(不重复的)字符序列的末尾。fast
指针:用于遍历整个字符数组。
- 我们使用两个指针:
- 遍历过程:
- 我们用
fast
指针从头到尾遍历字符数组。
- 我们用
- 字符处理:
- 对于每个
fast
指向的字符,我们都将其复制到slow
指针的位置:chars[slow] = chars[fast]
。 - 这一步确保了我们不会漏掉任何字符,同时为后续的去重操作做准备。
- 对于每个
- 去重逻辑:
- 在复制字符后,我们检查是否需要去除重复:
- 如果
slow > 0
(确保有前一个字符可比较)且当前字符与前一个字符相同(chars[slow] == chars[slow - 1]
), 我们就将slow
指针后退一步(slow--
)。这有效地删除了这对重复的字符。 - 如果不满足上述条件,说明当前字符不需要删除,我们就将
slow
指针前进一步(slow++
)。
- 如果
- 在复制字符后,我们检查是否需要去除重复:
- 结果生成:
- 遍历结束后,
slow
指针的位置恰好是去重后字符串的长度。 - 我们使用
new String(chars, 0, slow)
创建一个新的字符串,它包含了字符数组从索引0到slow(不包括slow)的所有字符, 这就是我们的最终结果。
- 遍历结束后,
代码实现
public String removeDuplicates(String s) {
// 将字符串转换为字符数组
char[] chars = s.toCharArray();
int slow = 0;// 慢指针,指向当前处理的位置
// 快指针遍历整个字符数组
for (int fast = 0; fast < chars.length; fast++){
// 将当前字符放到慢指针的位置
chars[slow] = chars[fast];
if (slow > 0 && chars[slow] == chars[slow - 1]){
// 如果当前字符与前一个字符相同,慢指针回退
slow -- ;
}else {
// 否则,慢指针前进
slow++;
}
}
// 返回处理后的字符串,长度为慢指针的位置
return new String(chars,0,slow);
}
解法二: {{字符串直接作为栈}}
解题思路
我们在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素,那么如何记录前面遍历过的元素呢?
所以就是用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。
然后再去做对应的消除操作。 如动画所示:
从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。
代码实现
public String removeDuplicates1(String s) {
StringBuffer res = new StringBuffer();
int top = -1;
for (int i = 0 ; i < s.length();i++){
char c = s.charAt(i);
if (top >= 0 && res.charAt(top) == c){
res.deleteCharAt(top);
top--;
}else {
res.append(c);
top++;
}
}
return res.toString();
}