目录
方法一 利用栈(Stack类)和映射(Map接口),栈中只存左括号
方法二 只利用栈(Stack类),栈只存与左括号对应的右括号
方法三 只利用栈(Deque接口),栈只存与左括号对应的右括号
方法一 使用ArrayDeque实现栈,用StringBuffer拼接栈内字符串并倒序
【20. 有效的括号】
方法一 利用栈(Stack类)和映射(Map接口),栈中只存左括号
思路:
1、构建左括号为key,右括号为value的map,用于判断是否匹配
2、构建栈,遍历输入的字符串s
如果遇到的是左边的括号,就压入栈
如果遇到的是右边的括号,就判断是否能匹配
- 如果栈内为空,即没有可匹配的左括号,则直接返回false
- 如果栈内不为空
如果右边的括号匹配栈顶的左括号,弹出被匹配的左括号
如果右边的括号不匹配栈顶的左括号,直接返回false
4、如果遍历完之后,栈内还剩左括号,证明没匹配完,返回false
class Solution {
public boolean isValid(String s) {
// 如果输入的括号数是奇数,则直接返回false
if (s.length() % 2 == 1) return false;
// 1、构建左括号为key,右括号为value的map,用于判断是否匹配
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 c = s.charAt(i);
// 2、如果遇到的是左边的括号,就压入栈
if (map.containsKey(c)){
stack.push(c);
}
// 3、如果遇到的是右边的括号,就判断是否能匹配
else{
// -如果栈内没有左括号,即没有可匹配的左括号,则直接返回false
if (stack.size() == 0) return false;
// -如果栈内有左括号
char top = stack.peek(); // 获取处于栈顶的左括号
// --如果右边的括号匹配栈顶的左括号,弹出被匹配的左括号
if (map.get(top).equals(c)){
stack.pop();
}
// --如果右边的括号不匹配栈顶的左括号,直接返回false
else{
return false;
}
}
}
// 遍历完之后,如果栈内为空,则返回true,如果栈内还剩左括号,证明没匹配完,返回false
return stack.isEmpty();
}
}
时间复杂度: O(n),要遍历字符串为O(n),哈希表和栈的操作都为O(1)
空间复杂度: O(n),map固定长度3为O(1),stack最大为O(n)
方法二 只利用栈(Stack类),栈只存与左括号对应的右括号
思路:
1、构建栈,遍历输入的字符串s
如果是左括号,就把其对应的右括号压入栈中
如果都不是左括号,那剩下的就是右括号,需要考虑匹配问题
- 如果不匹配,则返回false
- 如果匹配,则将栈顶与其相等的右括号弹出
2、如果遍历完之后,栈内还有右括号,证明没匹配完,返回false
class Solution {
public boolean isValid(String s) {
// 如果s的长度是奇数,不可能完全匹配,则直接返回false
if (s.length() % 2 == 1) return false;
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++){
char c = s.charAt(i);
// 如果是左括号,就把对应的右括号压入栈中
if (c == '{'){
stack.push('}');
}
else if (c == '['){
stack.push(']');
}
else if (c == '('){
stack.push(')');
}
// 如果都不是左括号,那剩下的就是右括号,需要考虑匹配问题
// - 如果不匹配
else if (stack.size() == 0 || c != stack.peek()){
return false;
}
// - 如果匹配
else{
stack.pop();
}
}
return stack.isEmpty();
}
}
时间复杂度: O(n),要遍历字符串为O(n),栈的操作都为O(1)
空间复杂度: O(n),stack最大为O(n)
方法三 只利用栈(Deque接口),栈只存与左括号对应的右括号
【Deque接口的使用】
1、Deque接口的介绍:
- Java中的Deque接口继承自Queue接口,Deque接口有多个实现类,其中最常用的实现类是ArrayDeque和LinkedList。
- ArrayDeque是基于数组实现的双端队列,而LinkedList是基于链表实现的双端队列。
Deque<> deque = new ArrayDeque<>(); // 基于数组实现
Deque<> deque = new LinkedList<>(); // 基于链表实现
2、Deque接口定义的常用方法:
- 针对<栈>的方法:push()和pop()
- 针对<队列>的方法:offer()和poll()
因此,既可以用Deque实现栈,也可以用Deque实现队列。
注意,Queue接口只有针对队列的方法,如offer()和poll()。
class Solution {
public boolean isValid(String s) {
// 如果s的长度是奇数,不可能完全匹配,则直接返回false
if (s.length() % 2 == 1) return false;
Deque<Character> stack = new LinkedList<>();
for (int i = 0; i < s.length(); i++){
char c = s.charAt(i);
// 如果是左括号,就把对应的右括号压入栈中
if (c == '{'){
stack.push('}');
}
else if (c == '['){
stack.push(']');
}
else if (c == '('){
stack.push(')');
}
// 如果都不是左括号,那剩下的就是右括号,需要考虑匹配问题
// - 如果不匹配
else if (stack.size() == 0 || c != stack.peek()){
return false;
}
// - 如果匹配
else{
stack.pop();
}
}
return stack.isEmpty();
}
}
时间复杂度: O(n),要遍历字符串为O(n),栈的操作都为O(1)
空间复杂度: O(n),stack最大为O(n)
注意:与方法二的区别只在于创建栈的时候,使用的实现类不同。
- 方法二:Stack<Character> stack = new Stack<>(); // 基于Stack类实现
- 方法三:Deque<Character> stack = new LinkedList<>(); // 基于Deque接口和LinkedList实现类实现
【1047. 删除字符串中的所有相邻重复项】
方法一 使用ArrayDeque实现栈,用StringBuffer拼接栈内字符串并倒序
class Solution {
public String removeDuplicates(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if (stack.size() != 0 && c == stack.peek()){
stack.pop();
}
else{
stack.push(c);
}
}
StringBuffer sb = new StringBuffer();
while (!stack.isEmpty()){
sb.append(stack.pop());
}
sb.reverse();
return sb.toString();
}
}
时间复杂度: O(n)
空间复杂度: O(n)
【注意:当在两端快速插入和删除元素、高效访问时,ArrayDeque比LinkedList更有优势】
ArrayDeque 具有以下特点:
快速随机访问:由于 ArrayDeque 基于数组实现,可以通过索引直接访问数组中的元素,因此随机访问的性能非常高,时间复杂度为 O(1)。
高效的头部和尾部操作:ArrayDeque 可以在数组的头部和尾部进行高效的插入和删除操作,时间复杂度也为 O(1)。这是因为 ArrayDeque 在内部维护了两个指针,分别指向数组的头部和尾部,通过这两个指针可以快速插入和删除元素。当需要在头部或尾部插入或删除元素时,只需移动对应的指针即可。
而 LinkedList 虽然插入和删除的时间复杂度也为 O(1) ,但其访问的时间复杂度为 O(n) 。
方法二 直接使用StringBuffer充当栈,不需要倒序
class Solution {
public String removeDuplicates(String s) {
StringBuffer stack = new StringBuffer();
for (int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if (stack.length() != 0 && stack.charAt(stack.length() - 1) == c){
stack.deleteCharAt(stack.length() - 1);
}
else{
stack.append(c);
}
}
return stack.toString();
}
}
时间复杂度: O(n),如果stack不需要扩容,append的时间复杂度为O(1)的情况下
空间复杂度: O(n)
注意:StringBuffer类删除指定索引字符的方法是deleteCharAt(int idx),如果是delete(int start, int end)方法,必须传入需要删除字符串的开始索引和结束索引,规则是左闭右开。
【总结:针对不同的数据类型,判断内容是否相等的方法】
- 基本数据类型之间的比较:== 运算符
- 引用数据类型之间的比较:equals() 方法
- 包装数据类型之间的比较:equals() 方法
- 基本数据类型与包装数据类型之间的比较:使用 == 时,会自动先将包装类型自动拆箱为基本类型,然后再进行内容比较。包装数据类型的变量使用 equals() 方法,也可以进行内容比较。
方法三 拓展 双指针(比较难理解)
思路:快慢指针配合用字符数组记录结果,排除相邻的两字母
- fast指针负责遍历和读值
- slow指针负责排除相邻的两字母,查找可以写值的位置,以及进行写值
class Solution {
public String removeDuplicates(String s) {
int slow = 0;
int fast = 0;
char[] ch = s.toCharArray();
while (fast < s.length()){
// 将fast读到的值写在slow指向的位置上
ch[slow] = ch[fast];
// 利用slow辨别匹配项,并重新选择可以写值的位置
if (slow > 0 && ch[slow] == ch[slow - 1]){
slow--; // 如果出现两字母相邻且相同,则slow回退一步
}
else{
slow++; // 如果未出现两字母相邻且相同,则slow继续遍历
}
// fast继续遍历
fast++;
}
return new String(ch, 0, slow); // 不包含ch[slow]
}
}
时间复杂度: O(n),双指针遍历整个字符数组
空间复杂度: O(n),使用额外的字符数组,复杂度为O(n)
【150. 逆波兰表达式求值】
思路:
1、创建栈
2、遍历字符串数组
- 如果是运算符,则弹出栈顶的前两个元素按运算符运算
- 如果不是运算符,则将字符串转成整型,再压入栈
3、最后剩下的元素就是计算结果
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < tokens.length; i++){
String s = tokens[i];
// 如果是运算符,则弹出栈顶的前两个元素按运算符运算
if (s.equals("+")){
stack.push(stack.pop() + stack.pop());
}
else if (s.equals("-")){
stack.push(- (stack.pop() - stack.pop()));
}
else if (s.equals("*")){
stack.push(stack.pop() * stack.pop());
}
else if (s.equals("/")){
int x = stack.pop();
stack.push(stack.pop() / x);
}
// 如果不是运算符,则将字符串转成整型,再压入栈
else{
stack.push(Integer.valueOf(s));
}
}
return stack.pop(); // 最后剩下的元素就是计算结果
}
}
时间复杂度: O(n)
空间复杂度: O(n)
总结:
1、字符串与包装类之间的转换
- 字符串 -> 包装类:Integer x = Integer.valueOf ( String s );
- 包装类 -> 字符串:String s = Integer.toString ( Integer x );
2、自动装箱与自动拆箱
自动装箱:基本类型自动转为包装类型,如:Integer x = 1;
自动拆箱:包装类型自动转为基本类型,如:int y = x;
(代码最后return的是Integer类型的值,而方法规定的返回值是int型,这里隐含了自动拆箱。)