Problem: 20. 有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
整体思路
这段代码旨在解决一个非常经典的计算机科学问题:有效的括号 (Valid Parentheses)。问题要求判断一个只包含 ( ) { } [ ]
六种括号的字符串是否是“有效”的。有效性有两个标准:1. 左括号必须用相同类型的右括号闭合。2. 左括号必须以正确的顺序闭合。
该算法采用的是解决此类问题的标准且最高效的方法:栈 (Stack)。其核心逻辑思想是,利用栈“后进先出”(LIFO)的特性来完美匹配括号的嵌套结构。
-
预处理与数据结构选择:
- 快速失败:代码首先进行了一个简单的优化判断
s.length() % 2 != 0
。一个有效的括号字符串,其长度必然是偶数。如果长度为奇数,可以直接判定为无效,提前返回false
。 - 栈的选择:算法的核心是一个栈(在Java中通过
Deque
接口和ArrayDeque
实现类来创建)。这个栈用来存储所有遇到的、但尚未匹配到相应右括号的左括号。 - 匹配关系:为了方便地查找一个右括号对应的左括号,代码使用了一个
HashMap
。Map
的键是右括号,值是对应的左括号。这使得匹配查询操作的效率为 O(1)。
- 快速失败:代码首先进行了一个简单的优化判断
-
单次遍历与匹配逻辑:
- 算法通过一次
for
循环遍历输入字符串s
中的每一个字符c
。 - 遇到左括号:如果当前字符
c
是一个左括号(通过!mp.containsKey(c)
判断,因为只有右括号是Map
的键),就将其压入栈中,等待后续的右括号来匹配。 - 遇到右括号:如果当前字符
c
是一个右括号,则进入匹配逻辑:
a. 栈空检查:首先检查栈是否为空。如果为空,说明遇到了一个右括号,但之前没有任何未匹配的左括号,这显然是无效的(例如")"
或"{})"
)。
b. 类型匹配:如果栈不为空,就从栈顶弹出一个元素(st.pop()
)。这个弹出的元素应该是最近遇到的、未闭合的左括号。然后,将这个弹出的左括号与当前右括号c
对应的正确左括号(mp.get(c)
)进行比较。如果它们不相等,说明括号类型不匹配(例如"(]"
),字符串无效。
- 算法通过一次
-
最终校验:
- 当遍历完整个字符串后,如果字符串是完全有效的,那么所有的左括号都应该找到了对应的右括号并被从栈中弹出。因此,最后需要检查栈是否为空 (
st.isEmpty()
)。 - 如果栈为空,说明所有括号都完美匹配,字符串有效。
- 如果栈不为空,说明有剩余的左括号没有被闭合(例如
"("
或"{[()"
),字符串无效。
- 当遍历完整个字符串后,如果字符串是完全有效的,那么所有的左括号都应该找到了对应的右括号并被从栈中弹出。因此,最后需要检查栈是否为空 (
完整代码
class Solution {
/**
* 判断一个只包含 '(', ')', '{', '}', '[' 和 ']' 的字符串是否有效。
* @param s 输入的括号字符串
* @return 如果字符串有效则返回 true,否则返回 false
*/
public boolean isValid(String s) {
// 快速失败优化:有效的括号字符串长度必为偶数。
if (s.length() % 2 != 0) {
return false;
}
// 创建一个 Map 存储括号的匹配关系(key:右括号, value:左括号)。
// 使用实例初始化块(双大括号语法)来方便地初始化 Map。
Map<Character, Character> mp = new HashMap<>() {
{
put(')', '(');
put('}', '{');
put(']', '[');
}
};
// 使用 Deque 作为栈,这是 Java 中推荐的栈实现方式。
// st 用于存储所有遇到的、但尚未匹配的左括号。
Deque<Character> st = new ArrayDeque<>();
// 遍历字符串中的每一个字符
for (char c : s.toCharArray()) {
// 如果当前字符 c 是一个左括号(即它不是 Map 的键)
if (!mp.containsKey(c)) {
// 将左括号压入栈中
st.push(c);
}
// 如果当前字符 c 是一个右括号
else if (st.isEmpty() || st.pop() != mp.get(c)) {
// 检查两种无效情况:
// 1. st.isEmpty(): 栈中没有左括号来匹配当前的右括号 (例如 "())")
// 2. st.pop() != mp.get(c): 栈顶的左括号与当前右括号类型不匹配 (例如 "(]")
// 如果任一情况为真,则字符串无效。
return false;
}
}
// 遍历完所有字符后,如果栈为空,说明所有括号都完美匹配。
// 如果栈不为空,说明有未闭合的左括号。
return st.isEmpty();
}
}
时空复杂度
时间复杂度:O(N)
- 字符串遍历:算法的核心是一个
for
循环,它遍历输入字符串s
的所有字符一次。如果字符串的长度为N
,那么循环将执行N
次。 - 循环内部操作:
mp.containsKey()
:HashMap
的查找操作,平均时间复杂度为 O(1)。st.push()
:ArrayDeque
作为栈的压入操作,均摊时间复杂度为 O(1)。st.isEmpty()
: 检查是否为空,时间复杂度为 O(1)。st.pop()
:ArrayDeque
作为栈的弹出操作,均摊时间复杂度为 O(1)。mp.get()
:HashMap
的获取操作,平均时间复杂度为 O(1)。
- 综合分析:
- 整个算法由
N
次 O(1) 的操作组成。因此,总的时间复杂度是线性的,即 O(N)。
- 整个算法由
空间复杂度:O(N)
- 主要存储开销:算法的额外空间主要由
Map
和Deque
(栈) 占用。Map<Character, Character> mp
: 这个Map
存储了固定数量的键值对(3对)。其大小与输入字符串的长度N
无关,因此它占用的空间是常数级别的,即 O(1)。Deque<Character> st
: 这个栈用于存储未匹配的左括号。在最坏的情况下,输入字符串可能全是左括号,例如"((((..."
。在这种情况下,栈的大小会增长到与输入字符串长度N
相同。
综合分析:
算法所需的额外空间主要由栈 st
的大小决定。因此,在最坏情况下,空间复杂度为 O(N)。
参考灵神