算法通过村第四关——栈的经典算法问题(白银)
1.括号匹配问题
方法1:栈(使用map)
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
我们使用HashMap对象map来存储括号的对应关系,键表示左括号,值表示与之匹配的右括号。例如,‘(‘对应’)’,‘[‘对应’]’,‘{‘对应’}’。
我们创建一个Stack对象stack来辅助判断括号是否匹配。遍历输入的括号字符串时,执行以下操作:
- 如果当前字符是左括号,将其压入栈中。
- 如果当前字符不是左括号,先检查栈是否为空。如果栈不为空,说明之前已经有左括号被压入栈中,此时从栈顶取出一个左括号,并与当前字符进行匹配。如果匹配成功,则继续遍历下一个字符;如果匹配失败,即右括号与栈顶的左括号不匹配,那么字符串就是无效的,返回false。
- 如果栈为空,并且当前字符不是左括号,说明字符串中存在右括号没有与之匹配的左括号,返回false。
最后,检查栈是否为空。如果栈为空,说明字符串中的所有括号都是有效的,返回true;如果栈不为空,说明存在一些左括号没有与之匹配的右括号,返回false。
class Solution {
public boolean isValid(String s) {
if(s.length() <= 1){
return false;
}
Map<Character, Character> map = new HashMap<>();
map.put('(',')');
map.put('[',']');
map.put('{','}');
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char item = s.charAt(i);
if (map.containsKey(item)) {
stack.push(item);
} else {
if (stack.isEmpty()) {
return false;
}
char left = stack.pop();
if (map.get(left) != item) {
return false;
}
}
}
return stack.isEmpty();
}
}
空间复杂度是O(n),其中n是输入字符串s的长度。主要的空间消耗来自于栈的使用,栈的最大深度取决于括号的嵌套层数。
时间复杂度也是O(n),其中n是输入字符串s的长度。遍历整个字符串需要O(n)的时间,而在每次遍历过程中,对栈进行push和pop操作的时间复杂度都是O(1)。
方法2:栈(不使用map)
- 创建一个空栈。
- 遍历字符串的每个字符:
- 如果字符是左括号,则将其压入栈中。
- 如果字符是右括号,则判断栈是否为空,或者栈顶元素不是对应的左括号,如果是,则返回false;否则,弹出栈顶元素。
- 最后判断栈是否为空,如果为空,则说明所有的左括号都有对应的右括号,是有效字符串;否则,说明还有未闭合的左括号,不是有效字符串。
这种方法的时间复杂度为O(n),其中n是字符串的长度。
以下是使用栈实现的代码示例:
import java.util.Stack;
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else if (c == ')' && !stack.isEmpty() && stack.peek() == '(') {
stack.pop();
} else if (c == '}' && !stack.isEmpty() && stack.peek() == '{') {
stack.pop();
} else if (c == ']' && !stack.isEmpty() && stack.peek() == '[') {
stack.pop();
} else {
return false;
}
}
return stack.isEmpty();
}
这种方法使用了栈来维护左括号的顺序,遇到右括号时,只需要判断栈顶元素是否是对应的左括号,如果是,则弹出栈顶元素;否则,返回false。最后判断栈是否为空来确定字符串的有效性。这种方法的时间复杂度更低,并且代码更简洁。
方法3:使用Deque
使用了双端队列(Deque)的实现类 LinkedList 来作为栈的数据结构。代码逻辑与之前使用栈的解法是一样的。
public boolean isValid(String s) {
Deque<Character> stack = new LinkedList<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '(') {
stack.push(')');
} else if (ch == '[') {
stack.push(']');
} else if (ch == '{') {
stack.push('}');
} else if (stack.isEmpty() || stack.peek() != ch) {
return false;
} else {
stack.pop();
}
}
return stack.isEmpty();
}
这段代码的时间复杂度同样为O(n),其中n是字符串的长度。使用双端队列实现栈,可以更方便地进行元素的插入和删除操作。
2. 最小栈
1. 什么是辅助栈?
思考这类题之前,我们先提出一个概念:
辅助栈:
辅助栈是在解决某个问题或优化某个算法时使用的一个额外的数据结构。它通常用于存储一些中间结果或辅助信息,以方便实现某些操作或减少时间复杂度。
辅助栈经常用在需要快速获取一些中间结果或辅助信息的算法题中。
一些常见的算法题类型包括:
- 最小栈/最大栈:需要在常数时间内获取栈中的最小/最大元素。
- 括号匹配:需要判断给定字符串中的括号是否匹配。辅助栈可以用来存储左括号,检查右括号时与栈顶元素进行匹配。
- 表达式求值:需要对给定的数学表达式进行求值。辅助栈可以用来存储运算符和操作数,在遍历表达式时进行相应的计算。
- 进制转换:需要将给定的十进制数转换为其他进制。辅助栈可以用来存储余数,依次弹出并拼接得到转换结果。
这类算法题的共性是都需要在某些操作中快速获取一些中间结果或辅助信息。通过使用辅助栈,可以在常数时间内实现这些操作,提高算法的效率。另外,辅助栈的空间复杂度往往与问题规模相关,可能会占用额外的空间。
2. 设计数据结构
在最小栈的例子中,辅助栈用于存储当前栈中的最小元素。
对于最小栈来说,辅助栈的定义可以是一个单调不增(非严格递减)的栈。也就是说,辅助栈的栈顶元素始终代表着当前栈中的最小值。通过维护辅助栈的定义,我们可以在O(1)的时间复杂度内获取最小值,并保持这个操作在整个操作序列中都有效。
因为是java语言,java有一个接口Deque(双端队列),使用Deque
(双端队列)作为辅助栈是因为Deque
可以同时支持栈和队列的操作。Deque
是一个接口,我们可以通过具体的实现类来使用它,比如LinkedList
。
使用
Deque
作为辅助栈有以下几个优点:
Deque
既可以从头部进行元素的插入和删除(栈的操作),也可以从尾部进行元素的插入和删除(队列的操作)。这样我们即可以满足栈的后进先出的特性,也可以满足队列的先进先出的特性。- 使用
Deque
作为辅助栈可以更方便地实现栈相关的操作,如push
(入栈)、pop
(出栈)、peek
(获取栈顶元素)等。Deque
内部的实现通常是基于链表或动态数组,在进行元素的插入和删除时,时间复杂度为O(1)。这样可以保证最小栈的操作都能在O(1)的时间复杂度内完成。
Deque
接口提供了一系列用于操作双端队列的方法。下面是Deque
接口中常用的方法:
- 添加元素的操作:
void addFirst(E e)
:将元素插入到双端队列的头部。void addLast(E e)
:将元素插入到双端队列的尾部。boolean offerFirst(E e)
:将元素插入到双端队列的头部,如果成功则返回true
,否则返回false
。boolean offerLast(E e)
:将元素插入到双端队列的尾部,如果成功则返回true
,否则返回false
。
- 弹出元素的操作:
E removeFirst()
:移除并返回双端队列的头部元素,如果双端队列为空,则抛出异常。E removeLast()
:移除并返回双端队列的尾部元素,如果双端队列为空,则抛出异常。E pollFirst()
:移除并返回双端队列的头部元素,如果双端队列为空,则返回null
。E pollLast()
:移除并返回双端队列的尾部元素,如果双端队列为空,则返回null
。
- 获取元素但不移除的操作:
E getFirst()
:获取双端队列的头部元素,如果双端队列为空,则抛出异常。E getLast()
:获取双端队列的尾部元素,如果双端队列为空,则抛出异常。E peekFirst()
:获取双端队列的头部元素,如果双端队列为空,则返回null
。E peekLast()
:获取双端队列的尾部元素,如果双端队列为空,则返回null
。
- 其他操作:
boolean isEmpty()
:判断双端队列是否为空。int size()
:返回双端队列中的元素个数。void clear()
:清空双端队列中的所有元素。
需要注意的是,Deque
既可以作为栈使用(后进先出),也可以作为队列使用(先进先出)。
3. 解题思路
-
创建两个空的辅助栈xStack和minStack,并将minStack初始化为只包含一个Integer.MAX_VALUE,表示栈中暂时没有元素。
- 首先,我们需要创建两个
Deque
对象来作为辅助栈,分别命名为xStack和minStack。 - 两个空的辅助栈xStack和minStack在解决最小栈问题时的作用如下:
- xStack:xStack是存储实际元素的主要栈,用于执行push、pop和top操作。所有的元素都会被压入和弹出xStack。
- minStack:minStack是存储当前栈中的最小元素的辅助栈。它的作用是在每次push操作时维护当前栈的最小值,并且能够在O(1)的时间复杂度内获取最小值。
- 初始化minStack时,将其压入一个初始值,这里选择Integer.MAX_VALUE,表示当前栈中没有元素。
class MinStack { Deque<Integer> xStack; Deque<Integer> minStack; public MinStack() { xStack = new LinkedList<Integer>(); minStack = new LinkedList<Integer>(); minStack.push(Integer.MAX_VALUE); } }
- 首先,我们需要创建两个
-
push操作:将元素x压入xStack,并将minStack的栈顶元素与x比较,取较小值压入minStack。
- 需要将元素x压入xStack,即调用
xStack.push(x)
。 - 取出minStack的栈顶元素与x比较,如果x小于等于minStack的栈顶元素,则将x压入minStack,否则将minStack的栈顶元素再次压入minStack。这样可以保证minStack始终存储着当前栈中的最小值。
public void push(int x) { xStack.push(x); minStack.push(Math.min(minStack.peek(), x)); }
- 需要将元素x压入xStack,即调用
-
pop操作:分别从xStack和minStack中弹出栈顶元素,实现出栈操作。
- 需要同时从xStack和minStack中进行出栈操作,即分别调用
xStack.pop()
和minStack.pop()
。
public void pop() { xStack.pop(); minStack.pop(); }
- 需要同时从xStack和minStack中进行出栈操作,即分别调用
-
top操作:返回xStack的栈顶元素,即当前栈顶元素。
- 需要返回xStack的栈顶元素,即调用
xStack.peek()
。
public int top() { return xStack.peek(); }
- 需要返回xStack的栈顶元素,即调用
-
getMin操作:返回minStack的栈顶元素,即最小值。
- 需要返回minStack的栈顶元素,即调用
minStack.peek()
。
public int getMin() { return minStack.peek(); }
- 需要返回minStack的栈顶元素,即调用
通过以上步骤,我们就能够实现一个支持push、pop、top和getMin操作的最小栈。其中,利用辅助栈minStack来存储当前栈中的最小元素,并且保证获取最小值的时间复杂度为O(1)。
完整代码:
class MinStack {
Deque<Integer> xStack;
Deque<Integer> minStack;
public MinStack() {
xStack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE);
}
public void push(int val) {
xStack.push(val);
minStack.push(Math.min(minStack.peek(), val));
}
public void pop() {
xStack.pop();
minStack.pop();
}
public int top() {
return xStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
3.最大栈
LeetCode 716.设计一个最大栈数据结构,既支持栈操作,又支持查找栈中最大元素。
其实跟上道题一样的,主要是解决peekMax()和popMax()这两个方法
方法一:使用Stack作为辅助栈
class MaxStack {
Stack<Integer> stack;
Stack<Integer> maxStack;
public MaxStack() {
stack = new Stack();
maxStack = new Stack();
}
public void push(int x) {
int max = maxStack.isEmpty() ? x : maxStack.peek();
maxStack.push(max > x ? max : x);
stack.push(x);
}
public int pop() {
maxStack.pop();
return stack.pop();
}
public int top() {
return stack.peek();
}
public int peekMax() {
return maxStack.peek();
}
public int popMax() {
int max = peekMax();
Stack<Integer> buffer = new Stack();
while (top() != max) buffer.push(pop());
pop();
while (!buffer.isEmpty()) push(buffer.pop());
return max;
}
}
方法二:使用Deque作为辅助栈
几乎一模一样滴~
class MaxStack {
Deque<Integer> stack;
Deque<Integer> maxStack;
public MaxStack() {
stack = new LinkedList<>();
maxStack = new LinkedList<>();
}
public void push(int x) {
stack.push(x);
if (maxStack.isEmpty()) {
maxStack.push(x);
} else {
maxStack.push(Math.max(x, maxStack.peek()));
}
}
public int pop() {
maxStack.pop();
return stack.pop();
}
public int top() {
return stack.peek();
}
public int peekMax() {
return maxStack.peek();
}
public int popMax() {
int max = peekMax();
Deque<Integer> buffer = new LinkedList<>();
while (top() != max) buffer.push(pop());
pop();
while (!buffer.isEmpty()) push(buffer.pop());
return max;
}
}