算法刷题系列(十九):括号问题——用栈与前缀约束检查
算法刷题系列(十九):括号问题——用栈与前缀约束检查
算法刷题系列(十九):括号问题——用栈与前缀约束检查
栈篇介绍了后进先出的结构,表达式计算也用栈管理待处理状态。本篇把栈应用到括号:最近遇到的未匹配左括号必须最先与右括号配对。
括号题有几个不同问题,不能混为一谈:
- 多种括号是否完整匹配;
- 只有圆括号时,最长有效连续子串有多长;
- 如何根据题目规则生成括号组合。
本文重点解决前两类,并解释它们分别依赖的状态。
一、多类型括号匹配:栈保存未闭合项
扫描字符串时:
- 遇到左括号,把它压入栈;
- 遇到右括号,检查栈顶是否为对应类型;
- 类型不一致或栈为空,立即判定不合法;
- 扫描结束后,栈必须为空。
栈顶是最近尚未闭合的左括号,因此嵌套结构能按后进先出的顺序匹配。
例如:
1
2
{[()]} 合法
{[(])} 不合法:遇到 ] 时,最近未闭合的是 (
二、Java:多类型括号检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.ArrayDeque;
import java.util.Deque;
public class Parentheses {
public static boolean isBalanced(String text) {
if (text == null) {
throw new IllegalArgumentException("text must not be null");
}
Deque<Character> stack = new ArrayDeque<>();
for (int i = 0; i < text.length(); i++) {
char current = text.charAt(i);
if (current == '(' || current == '[' || current == '{') {
stack.push(current);
} else if (current == ')' || current == ']' || current == '}') {
if (stack.isEmpty() || !matches(stack.pop(), current)) {
return false;
}
} else {
throw new IllegalArgumentException(
"text may contain only bracket characters"
);
}
}
return stack.isEmpty();
}
private static boolean matches(char opening, char closing) {
return (opening == '(' && closing == ')')
|| (opening == '[' && closing == ']')
|| (opening == '{' && closing == '}');
}
}
三、Python:多类型括号检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def is_balanced(text: str) -> bool:
if text is None:
raise ValueError("text must not be None")
pairs = {")": "(", "]": "[", "}": "{"}
openings = set(pairs.values())
stack: list[str] = []
for char in text:
if char in openings:
stack.append(char)
elif char in pairs:
if not stack or stack.pop() != pairs[char]:
return False
else:
raise ValueError("text may contain only bracket characters")
return not stack
assert is_balanced("{[()]}")
assert not is_balanced("{[(])}")
assert not is_balanced("(()")
assert is_balanced("")
时间复杂度为 O(n),最坏额外空间为 O(n)。
四、最长有效圆括号:栈记录边界
多类型匹配问的是整串是否合法;最长有效圆括号问的是某个连续子串的最长长度。这里有一个方便的栈技巧:栈存索引,并始终用栈顶表示当前有效段的左边界之前一个位置。
初始化栈为 [-1]。扫描到左括号就压入索引;扫描到右括号则弹出:
- 若栈空,把当前右括号索引压入,作为下一段的边界;
- 若栈不空,当前有效长度为
i - stack[-1]。
例如 ()(():最后一个右括号边界使前一段不能跨越不匹配的位置。
五、Java:最长有效圆括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.ArrayDeque;
import java.util.Deque;
public class LongestValidParentheses {
public static int longestValidParentheses(String text) {
if (text == null) {
throw new IllegalArgumentException("text must not be null");
}
Deque<Integer> indices = new ArrayDeque<>();
indices.push(-1);
int best = 0;
for (int i = 0; i < text.length(); i++) {
char current = text.charAt(i);
if (current == '(') {
indices.push(i);
} else if (current == ')') {
indices.pop();
if (indices.isEmpty()) {
indices.push(i);
} else {
best = Math.max(best, i - indices.peek());
}
} else {
throw new IllegalArgumentException(
"text may contain only '(' and ')'"
);
}
}
return best;
}
}
六、Python:最长有效圆括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def longest_valid_parentheses(text: str) -> int:
if text is None:
raise ValueError("text must not be None")
indices = [-1]
best = 0
for index, char in enumerate(text):
if char == "(":
indices.append(index)
elif char == ")":
indices.pop()
if not indices:
indices.append(index)
else:
best = max(best, index - indices[-1])
else:
raise ValueError("text may contain only '(' and ')'")
return best
assert longest_valid_parentheses(")()())") == 4
assert longest_valid_parentheses("(()") == 2
assert longest_valid_parentheses("") == 0
七、容易忽略的前提
最长有效圆括号的索引栈算法只适用于输入由 ( 和 ) 构成的字符串。多种括号混合时,不能只靠这个单栈边界公式求最长子串;需要根据题目定义重新设计状态。
常见错误:
- 只统计左右括号总数相等:
)(总数相等,但并不合法。 - 多种括号只记录数量:类型和嵌套顺序也必须正确。
- 忘记初始边界
-1:会漏掉从字符串开头开始的有效段。 - 遇到无法匹配的右括号后不重设边界:后面的有效段长度会跨过错误位置。
- 把整串匹配与最长子串混为一题:两者的返回目标和维护状态不同。
小结
多类型括号匹配用栈顶匹配最近的未闭合左括号;最长有效圆括号可用索引栈维护有效段边界。明确题目是“整串是否合法”还是“最长连续合法片段”,再选择合适的不变量。
本文由作者按照 CC BY 4.0 进行授权