今天我们来学习括号匹配算法。
作为程序员应该都知道,我们在码代码的过程中,会用到各类括号,无论是{}
还是()
还是[]
。括号都要求成对出现,要不然就会报语法错误。得益于强大的IDE,我们不需要自己去检测括号,如果多写或者漏写了括号,IDE就会提示我们有语法错误,我们改正一下就可以了。那你是否好奇过IDE的这个检测算法是如何实现的呢?
接下来我们来分析分析。
我们前面提到了可能会有多种括号情况:{}
、()
、[]
等等。我们先只取一种来分析,我们这里取()
。
见多识广
先从简单的开始:
0 * (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 —— ⑤
略加分析,我们也可以发现第四个式子错了,少了一个)
,第五个式子也错了,少了两个(
。
不知道大家有感觉没有。我们可以先把数字和其他运算符去掉,那么四个式子就变成了这样:
( ) —— ①
( ( ) —— ②
( ( ) ) —— ③
( ) ( ( ) —— ④
( ( ) ( ) ) ) ) —— ⑤
不难发现,我们从括号的个数上可以很快得出结论:
( ) —— ①
:(
个数为1,)
个数为1,总个数2
( ( ) —— ②
:(
个数为2,)
个数为1,总个数3
( ( ) ) —— ③
:(
个数为2,)
个数为2,总个数4
( ) ( ( ) —— ④
:(
个数为3,)
个数为2,总个数5
( ( ) ( ) ) ) ) —— ⑤
:(
个数为3,)
个数为5,总个数8
只要(
和)
的个数不匹配,那么这个表达式就不合法。
这个规律看起来没啥,因为本身匹配的定义就要求括号要成对出现,那么(
和)
的个数肯定是一样的。那么是否只要(
和)
个数相等就可以了呢,不会这么简单吧?
得来全不费工夫
是的,就是这么简单。我们来看,假设现在有一个括号序列,我们只知道它们的左右括号数量是相等的,数量各为n
,位置信息不清楚。那么肯定会有至少一对括号是彼此相邻的,就像这样:
......()......
.......
表示其他括号位置不清楚。
那么我们可以直接把这对括号去掉,类似于我们把括号内的表达式计算好了后,那么这对括号就不需要了,可以去除,那么剩下n-1
对括号。而n-1
对括号的问题不是正和n
对括号的问题是一样的吗?我们可以继续找到这样一对括号,把它去除,剩下n-2
对括号… …我们依次将所有的括号对去除掉,就类似于计算了一个完整的表达式。所以我们得到了这样的结论:如果左括号(例:(
)和右括号(例:)
)的个数相等,那么括号是匹配的,表达式合法。
高兴得太早
有了上面那个结论,可以很快把程序写出来:
public static boolean bracketsMatching(String expr) {
int codePointCount = expr.codePointCount(0, expr.length());
// 记录左括号比右括号多的次数
int cnt = 0;
for (int codePointIdx = 0; codePointIdx < codePointCount; codePointIdx++) {
int codePoint = expr.codePointAt(codePointIdx);
if (codePoint == '(' || codePoint == '[' || codePoint == '{') {
// 出现左括号,次数+1
cnt++;
} else if (codePoint == ')' || codePoint == ']' || codePoint == '}') {
// 出现右括号,次数-1
cnt--;
} else {
// 忽略其他字符
}
}
// 左括号比右括号多的次数为0,即左括号和右括号出现次数相等
return cnt == 0;
}
我们使用上面的例子来测试一下:
private static void checkMatching(String expr) {
System.out.println(expr + ": " + bracketsMatching(expr));
}
public static void main(String[] args) {
checkMatching("0 * (1 + 2)");
checkMatching("3 + 4 * (5 + 6 * (7 + 8)");
checkMatching("9 * (10 + 11 * (12 + 13)) + 14 ");
checkMatching("(15 + 16) * (17 + 18 * (19 + 20) ");
checkMatching("((21 + 22) * 23 + (24 * 25)) + 26 * 27)) + 28 ");
}
输出如下:
0 * (1 + 2): true
3 + 4 * (5 + 6 * (7 + 8): false
9 * (10 + 11 * (12 + 13)) + 14 : true
(15 + 16) * (17 + 18 * (19 + 20) : false
((21 + 22) * 23 + (24 * 25)) + 26 * 27)) + 28 : false
其中true
表示括号匹配,false
表示不匹配。
结果跟我们判断的情况一致。
很高兴,我们再用几个新的例子试一下,加入其它的括号:
checkMatching("({1 + (2 * 3)} * 4) * 5");
checkMatching("{(6 + 7} * 8) * (7 + 9)");
输出结果如下:
({1 + (2 * 3)} * 4) * 5: true
{(6 + 7} * 8) * (7 + 9): true
咦,第二个例子判断不对了,它的}
和)
错位了,但是如果按照左括号等于右括号即匹配会得出这个表达式括号匹配的结论,所以这里就有问题了。哎,前面得意的太早,问题不是那么简单的。
我们取这个式子{(6 + 7} * 8) * (7 + 9)
仔细分析下。问题出在了{ ( } )
这个部分,{}
和()
交错了。怎么理解呢?就是说**一个正常匹配的括号对{}
或()
,它们内部所包含的字符串只能是其他非字符,不能是其他类的括号字符。**而这个式子中,{}
和()
就是互相包含了其他类括号的字符,导致无法匹配了。而如果式子中只有一类括号,如我们前面举的5个例子中,只包含()
,那么之前的那个结论还是适用的。如果存在多个括号,就会出错,无法应对这种括号交错的问题了。
重新出发
我们再来重新审视这个问题:如果一对括号匹配,那么这对括号之间不能有其他的括号字符,如()
。
那么我们可以从某一类括号的角度出发找到所有的匹配括号,然后判断它们之间是否包含了其他括号字符,如果包含,则表达式括号不匹配,如果不包含,则匹配,如( { ) ( )
这个在第一对括号间包含了一个{
就不匹配的。
也就是说,在遇到一个左括号的时候,我们要保存接下来遇到的括号字符,直到遇到下一个相匹配的左括号。然后进行判断,如果有其他的括号字符,那么这个表达式就匹配,如果没有则不匹配。我们可以很快的想到用列表来保存遇到的括号数据,然后判断结束后,列表数据清空就可以。
但是很不巧,括号是可以嵌套的,如{ ( ) }
这样的情况。也就是说,我们会一直遇到各类左括号,如果只是单纯的去找相匹配的右括号,那么这种嵌套的括号就会被认为非法,这样显然不行。那么我们是不是可以把之前遇到的左括号先保存起来,先判断新的括号,确认ok后,清除最近匹配成功的左括号,再判断更早遇到的括号呢?
答案是可以的,而先判断新的再判断老的这种模式,不就是先进后出吗,我们很快就可以想到栈
这种数据结构。具体实现方式如下:
- 遇到左括号,入栈,继续执行。
- 遇到右括号,将栈顶元素弹出判断是否是相匹配的括号。
- 如果栈为空或返回元素为null(也表示栈为空),则返回表达式不匹配。
- 如果是,匹配,继续执行。
- 如果不是(是其他类括号的左括号,违背了在一对匹配的括号之间不能包含其他类的括号的原则),不匹配,直接返回表达式不匹配。
- 达到表达式末尾,如果栈中有剩余元素,表示存在未匹配的左括号,返回表达式不匹配,如果栈为空,返回表达式匹配。
还是以{(6 + 7} * 8)
为例,我们来看一下这个过程,我们省略其他无关字符,{ ( } )
:
4. 遇到{
,入栈,栈中元素{
。
5. 遇到(
,入栈,栈中元素{ (
。
6. 遇到}
,弹出栈顶元素(
,剩余栈中元素{
,}
与)
不匹配,直接返回表达式不匹配。
代码实现如下:
/**
* 多括号匹配算法
* @param expr 表达式
* @return 表达式是否合法
*/
public static boolean genernalizedBracketsMatching(String expr) {
int codePointCount = expr.codePointCount(0, expr.length());
// 记录左括号比右括号多的次数
Deque<Character> stack = new ArrayDeque<>(codePointCount >> 1);
for (int codePointIdx = 0; codePointIdx < codePointCount; codePointIdx++) {
int codePoint = expr.codePointAt(codePointIdx);
switch (codePoint) {
case '(':
case '[':
case '{':
// 左括号直接入栈
stack.push((char) codePoint);
break;
// 右括号判断栈中是否有元素,如无,则直接判定为不匹配
// 如有,继续弹出栈顶元素判断
case ')':
if (stack.isEmpty()) {
return false;
}
if (stack.pop() != '(') {
return false;
}
break;
case ']':
if (stack.isEmpty()) {
return false;
}
if (stack.pop() != '[') {
return false;
}
break;
case '}':
if (stack.isEmpty()) {
return false;
}
if (stack.pop() != '{') {
return false;
}
break;
default:
// 忽略其他字符
break;
}
}
// 如果栈为空,表示所有括号都匹配上了,如果不为空,则表示有左括号没有匹配上
return stack.isEmpty();
}
测试代码如下:
private static void checkGenernalizedMatching(String expr) {
System.out.println(expr + ": " + genernalizedBracketsMatching(expr));
}
public static void main(String[] args) {
checkGenernalizedMatching("0 * (1 + 2)");
checkGenernalizedMatching("3 + 4 * (5 + 6 * (7 + 8)");
checkGenernalizedMatching("9 * (10 + 11 * (12 + 13)) + 14 ");
checkGenernalizedMatching("(15 + 16) * (17 + 18 * (19 + 20) ");
checkGenernalizedMatching("((21 + 22) * 23 + (24 * 25)) + 26 * 27)) + 28 ");
checkGenernalizedMatching("({1 + (2 * 3)} * 4) * 5");
checkGenernalizedMatching("{(6 + 7} * 8) * (7 + 9)");
}
输出结果如下:
0 * (1 + 2): true
3 + 4 * (5 + 6 * (7 + 8): false
9 * (10 + 11 * (12 + 13)) + 14 : true
(15 + 16) * (17 + 18 * (19 + 20) : false
((21 + 22) * 23 + (24 * 25)) + 26 * 27)) + 28 : false
({1 + (2 * 3)} * 4) * 5: true
{(6 + 7} * 8) * (7 + 9): false
符合我们的预期。