ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

字符串匹配与栈:括号配对问题详解与C++实现

字符串匹配与栈:括号配对问题详解与C++实现 做 OJ 题看到“字符串匹配”千万别下意识以为是 KMP 那类子串查找。信息学奥赛一本通里的 1355 题题目名字确实叫“字符串匹配问题”但它的核心考点是用栈stack做括号配对。我第一次看到这题时也愣了半天字符串匹配怎么和栈扯上了关系后来想明白这里的“匹配”说的是左右符号的一一对应而“栈”恰好是这个场景最顺手的工具。这篇文章适合刚学栈的初学者也适合刷一本通卡在 1355 题的人。我会从题目意图讲起把为什么用栈、怎么写代码、容易踩哪些坑一次说清楚最后再给几个可以继续练的方向。不同 OJ 上这道题的输入格式可能略有差别差别主要在读入那一层核心的匹配函数完全可以复用。1. 题目到底在考什么字符串匹配与栈的握手1.1 先给这道题祛个魅它不是子串查找平时说“字符串匹配”很多人第一反应是在一个长串里找一个子串比如 KMP 算法、字符串哈希那一类问题。但一本通 1355 题完全不是这个意思。常见的题目设定是给你一行字符串里面混着英文小括号()、中括号[]、大括号{}让你判断这些括号是否成对、嵌套是否合法合法输出YES不合法输出NO。如果有其他普通字符比如字母、数字、空格、运算符号题目通常默认直接忽略它们不影响括号配对。比如a (b - c) * [d / e]这种串只看括号部分其实就是()和[]各自闭合最终结果是合法的。这种题很多地方也叫“括号匹配问题”“符号配对问题”。名字带“字符串”只是因为输入载体是一个字符串真正考的是栈的“后进先出”特性。把这一层想清楚后面代码写起来就顺了。1.2 为什么栈恰好适合括号配对栈stack是一种只允许在一端进行插入和删除操作的数据结构插入叫入栈删除叫出栈。它最大的特点就是后进先出后放进来的元素反而最先被拿出来。你可以把它想象成往圆筒里叠盘子最后放上去的盘子想用的时候一定是最先拿到的。括号配对也用到了同样的原则。当你从左往右扫描字符串时遇到一个右括号它需要配对的一定是“离它最近”的那个还没有配对的左括号。这个“最近”原则正好就是栈顶元素。举个例子字符串([])先遇到(入栈再遇到[入栈此时栈里从底部到顶部是(、[遇到]它应该匹配的是栈顶的[匹配成功弹出[遇到)此时栈顶是(匹配成功弹出(最后栈为空说明所有括号都配对完成。所以遇到左括号就压栈遇到右括号就检查栈顶配对成功就弹出配对失败或者栈为空就提前结束。这比手动记录所有左括号的位置要干净得多。1.3 先记住三个边界条件这道题的算法本身并不难难的是把边界情况想完整。我总结成三条遇到右括号时如果栈已经空了说明这个右括号没有对应的左括号直接输出不合法。遇到右括号时如果栈顶的左括号和当前右括号不是同一类型说明括号交叉嵌套了直接不合法。整个字符串扫描完之后如果栈里还有左括号说明有左括号一直没闭合也是不合法。只要把这三条逻辑对应到代码里这道题的核心就完成了。很多人在提交时反复出错往往不是不知道要用栈而是忘记处理第三条或者没写第二条的类型判断。2. 解题思路拆解三种括号的规则与匹配逻辑2.1 匹配规则逐条拆开来看我习惯把规则拆成五条来记这样写代码时不容易乱遇到左括号([{无条件入栈。遇到右括号)]}先看栈是否为空为空直接返回不合法。栈不为空就看当前右括号和栈顶左括号是不是同一类型。同一类型就弹出栈顶继续扫描下一个字符不是同一类型就返回不合法。整个字符串扫描结束后栈必须为空才返回合法。这里的“同一类型”要理解准确(必须配)[必须配]{必须配}。不能出现[和)配对、(和]配对这种混乱情况。普通字符的处理很简单扫描时判断当前字符是不是括号如果不是括号就继续走循环什么都不做。例如if (ch ( || ch [ || ch {)和else if (ch ) || ch ] || ch })这两个分支之外的字符全部忽略。2.2 一个关键问题为什么单纯计数会翻车新手最容易提出的疑问是我数一下左括号和右括号的个数是不是相等不也能判断吗答案是不行至少三种括号混合时不行。看字符串([)]。三种括号每种正好出现一次左右数量完全相等。如果你只统计数量会觉得它合法。但实际扫描一遍就知道第三个字符是)此时栈里有(和[栈顶是()确实能匹配(于是把(弹出接着第四个字符是]栈已经空了]没有左括号可配所以整个串不合法。也就是说([)]的问题在于出现了交叉嵌套。[本应该在最内层先闭合结果)先闭合了更外层的(破坏了括号的嵌套规则。单纯计数无意中对“顺序”视而不见自然会把这种错误串当成合法串。再看一个())。字符串())中左括号一个、右括号两个数量不等计数可能能发现。但如果是(()))这种数量已经肉眼可见不对有时候数量相等却依然非法比如)(左右各一个但第一个字符就是右括号栈为空立刻失败。所以计数只能作为辅助检查真正的判定必须依赖栈。2.3 复杂度与整体流程这道题的复杂度非常理想时间复杂度是 O(n)n 是字符串长度。每个括号最多入栈一次、出栈一次普通字符直接跳过所以扫描一遍就能完成判断。空间复杂度最坏是 O(n)也就是字符串全部由左括号组成时栈里会存下所有左括号。算法流程用文字描述就是定义一个空栈从左到右遍历字符串左括号入栈右括号和栈顶比较最后检查栈是否为空。整个过程是一个标准的线性扫描。3. 完整实现STL 版与手写栈版3.1 一个清晰的 STL stack 版完整代码我先把最常见的 STL 版本贴出来。这个版本直接用 C 标准库的stack代码简洁适合初学者理解逻辑。#include iostream #include string #include stack using namespace std; bool isMatch(char left, char right) { if (left ( right )) return true; if (left [ right ]) return true; if (left { right }) return true; return false; } bool check(const string s) { stackchar st; for (char ch : s) { if (ch ( || ch [ || ch {) { st.push(ch); } else if (ch ) || ch ] || ch }) { if (st.empty()) return false; if (!isMatch(st.top(), ch)) return false; st.pop(); } } return st.empty(); } int main() { string s; while (getline(cin, s)) { if (s.empty()) continue; cout (check(s) ? YES : NO) endl; } return 0; }注意isMatch函数的参数第一个是栈顶的左括号第二个是当前扫描到的右括号。如果匹配返回true。这样主函数里的判断逻辑就很简洁遇到右括号先判空再判断是否匹配最后弹出。main函数里我用getline(cin, s)读整行好处是字符串里如果包含空格也不会被截断。如果你本地 OJ 给出的输入是“一行只含一个字符串、没有空格”的形式用while (cin s)也可以。3.2 手写数组栈竞赛党的另一种习惯很多参加过信息学竞赛的同学不喜欢用 STL或者在一些老式评测环境里想尽量少依赖外部容器于是选择用数组模拟栈。这种做法不仅运行速度快也能让你对栈的底层结构有更直观的理解。bool isMatch(char left, char right) { if (left ( right )) return true; if (left [ right ]) return true; if (left { right }) return true; return false; } bool check(const string s) { char st[1005]; int top -1; for (char ch : s) { if (ch ( || ch [ || ch {) { st[top] ch; } else if (ch ) || ch ] || ch }) { if (top -1) return false; if (!isMatch(st[top], ch)) return false; top--; } } return top -1; }这里top -1表示栈空。入栈时先top再把字符存进st[top]出栈时直接top--。数组开 1005 基本够用因为这类题目的字符串长度一般不会超过 255。如果遇到更长的数据把数组开成 100005 也不会有任何压力。手写栈和 STL 栈相比本质逻辑一模一样差别只在于你如何管理栈顶指针。建议两种写法都练一遍既能理解数据结构原理也能应对不同竞赛环境。3.3 读入方式怎么选cin vs getline单组 vs 多组这道题在信息学奥赛一本通里通常是一行字符串输出一个结果但在不同 OJ 上可能有变化。我把常见的读入情况都列出来如果字符串不含空格直接cin s就行简单省事。如果字符串可能包含空格必须使用getline(cin, s)否则会只读到空格前的一段。如果题目先给一个整数n表示后面有n个字符串那么要用循环处理多组数据。如果题目要求一直读到文件结束那就用while (cin s)或while (getline(cin, s))。多组数据配合getline时要注意一个经典细节先用cin n读整数再用getline读字符串时整数后面的换行符会被getline吃掉。正确做法是中间加一次cin.ignore()。int n; cin n; cin.ignore(); // 吃掉 n 后面的换行 while (n--) { string s; getline(cin, s); cout (check(s) ? YES : NO) endl; }如果你的字符串一定不含空格那就直接用cin s连ignore都不用管。读入方式看起来只是小细节但很多人就是因为没处理换行符导致本地测试正常、提交后一直出错。4. 调试记录我踩过的坑与常见问题速查4.1 最容易翻车的是“栈空时取栈顶”我第一次写这题时脑子里的逻辑是遇到右括号先拿栈顶元素看看匹配就弹出不匹配就失败。听起来很顺但代码一旦写成这样就会在空栈上调用top()else if (ch ) || ch ] || ch }) { char topChar st.top(); // 这里 st 可能为空 if (!isMatch(topChar, ch)) return false; st.pop(); }当输入是)abc这种开头就是右括号的字符串时栈还是空的st.top()就是未定义行为。程序不一定会崩但结果完全不可控OJ 上可能表现为 RE 或者 TLE。正确做法是先判断st.empty()为空就直接返回false不要访问栈顶。手写栈也一样。数组栈中top -1就表示空此时访问st[top]就是访问st[-1]也就是数组越界。这种 bug 不一定会立刻崩溃但会让你怎么排查都找不到原因。4.2 匹配条件写反、漏掉最后判空还有一个很常见的错误是匹配条件写反。比如把isMatch(st.top(), ch)写成isMatch(ch, st.top())而isMatch内部又按“左括号在前右括号在后”来判断那么所有结果都会反掉。明明合法的()扫描到)时ch是)st.top()是(传参变成isMatch(), ()返回值永远是false于是合法串全部判成NO。漏掉最后判空也很常见。有些同学写完循环直接return true结果输入((())时扫描结束后栈里还留着一个左括号居然也输出YES。这显然不对。正确写法永远是return st.empty()它同时表达了“匹配成功”和“全部闭合”两个意思。4.3 常见问题速查表我把调试中比较高频的问题整理成一个速查表做题时如果卡住了可以对照着一项项排查现象可能原因处理办法合法样例输出 NO匹配条件写反或左右括号类型判断错误核对isMatch的参数方向和配对关系程序运行时崩溃右括号出现时栈为空仍然访问栈顶先判断st.empty()或top -1多组数据只处理了第一行外层循环缺失或用错读取方式按题面格式补循环注意cin.ignore()含空格的字符串判断错误使用cin s导致字符串被截断改用getline(cin, s)未闭合的左括号被判成合法循环结束直接返回true最后返回st.empty()或top -1手写栈偶发异常栈顶指针未初始化或越界访问初始top -1入栈先加出栈先判断为空表格里的最后一条其实也是大部分段错误问题的根源。代码逻辑再清晰只要指针越界一次整个程序都可能崩溃。4.4 给你一组可以直接跑的边界测试写完代码后建议先用一批边界用例验证。我常用的几组数据是按算法算结果应该是YES因为没有任何配对错误但实际 OJ 大概率不会给空行。()YES([])YES([)]NO((())NO())NO)(NO{a (b - c) * [d / e]}YES(({[]}))YES这些数据覆盖了空串、正常嵌套、交叉嵌套、未闭合、栈空遇右括号等情况。写完代码后不要急着交先手测一遍很多低级错误当场就能发现。4.5 调试时如何打印栈内容如果你想在本地看栈的变化过程可以临时在代码里加调试输出。STL 版本可以在入栈和出栈时打印栈顶。cerr push ch , stack size st.size() \n;用cerr而不是cout是因为有些 OJ 会区分标准输出和标准错误输出调试信息用cerr不会污染最终答案。提交前记得把调试输出删掉或者注释掉否则那堆调试文字会被当作答案输出直接 WA。5. 从这道题延伸出去的几个训练方向5.1 表达式括号校验与表达式求值把 1355 题的逻辑吃透之后可以进一步去练“表达式括号校验”和“表达式求值”。这两类题依然离不开栈。比如中缀表达式转后缀表达式的核心操作就是把运算符按优先级压入栈中遇到右括号再取出来。你可以把 1355 题的isMatch逻辑当成基础后续只是把“括号配对”换成“运算符优先级比较”。栈在处理括号类问题上的价值本质上是一套“记住最近状态”的机制。凡是遇到嵌套结构、括号结构、函数调用栈相关的模拟都可以想到栈。5.2 扩展自定义多字符标记的配对如果你觉得只匹配三个单字符不过瘾可以试着扩展成多字符标记的配对。比如判断div和/div是否正确闭合这就是简化版的 HTML/XML 标签校验。做法完全一致遇到开始标记时入栈遇到结束标记时出栈并对比标记名如果不匹配就说明结构有误。这个扩展题能帮你从“字符配对”升级到“字符串标记配对”理解更通用也更贴近真实开发中解析器、编译器的基本思路。注意结束标记里的/要过滤掉对比时只比标签名。5.3 继续刷题时的一点心得我个人觉得1355 题虽然只是栈的一道入门题但它把“最近匹配”“边界处理”“容器选型”三个点全揉进去了。很多新手刷题只追求通过不追究为什么结果遇到变式就懵。我建议把这题当成模板题来对待先手捏几组数据再手写一遍数组栈最后再改成 STL 版本三种方式都跑通。栈这个数据结构理解起来不难但真正用熟练需要时间。练完这题以后可以接着刷“进制转换”“字符串反转”“括号生成”“表达式求值”一类题目你会发现栈的应用远不止是配对这么简单。等回过头再来看这个题你已经不是那个只会背模板的人了。
返回列表