ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++回文判定实战:从双指针到OJ边界条件全解析

C++回文判定实战:从双指针到OJ边界条件全解析 1. 题目在考什么别被“基础题”三个字骗了东华OJ的89题“回文问题”在ACM题单里属于典型的入门字符串题。但如果你真觉得“回文嘛不就是正着读反着读都一样”然后草草写个两层循环交上去大概率会在一些不起眼的边界条件上翻车。我当年刷这道题的时候第一次提交就没过。不是因为思路不会而是因为没读懂题目到底在问什么。回文题看起来简单但它其实在考察三件事字符串的读取方式有没有问题尤其是有空格的情况字符比较的双向遍历逻辑是否写对了边界条件空串、单字符、长度奇偶有没有全覆盖而且东华OJ的判定向来比较严格它对输出格式、换行符、大小写都有明确的隐藏要求。换句话说这题不是“会不会写代码”的问题而是“代码能不能在严格判定下跑通”的问题。很多初学者在评论区问“我的代码明明本地跑得好好的为什么提交就是WA”原因往往不是逻辑错而是没有符合判题平台的预期输出格式。这一点我会在后面的排查部分展开细讲。2. 审题要点哪些细节决定了你的通过率2.1 经典回文串判定的“隐藏设定”因为在题目正文为空的情况下我不能看到东华OJ原题的具体描述所以这里基于最常见的OJ回文题设定来分析同时结合我刷题时见过的几个坑做一个全面的排查。东华OJ基础题的“回文问题”一般有两种常见问法输入一个字符串判断它是否为回文串是则输出YES否则输出NO。输入多个字符串逐个判断并统计回文串的个数或输出其中所有的回文串。结合题目编号89和“基础题”的定位第一种问法出现的概率最高。这类题你需要明确以下几点是否忽略空格例如“a b a”这种带空格的字符串原题如果要求整体判断那就不能直接把空格也算进字符序列。但大多数基础题里输入的是不含空格的连续字符串用cin直接读入即可。是否区分大小写有些题明确说明“忽略大小写”那就是A和a视为相同如果没说默认区分。输出格式是YES/NO还是yes/no东华OJ的基础题多用大写但也有例外。输出格式错了代码逻辑再对也是WA。这些细节在题目描述里通常只有一句话却是整个题的考点所在。我的经验是拿到回文题第一件事不是打开编辑器而是花两分钟把题目提到的“输入格式”和“输出格式”读三遍把每一个字都拆开看。2.2 输入读取cin、gets、getline到底选哪个C里读字符串的方法有好几种但用错了场景就会得到完全不同的结果。cin s适合读不含空格的连续字符串。遇到空格、Tab、换行都会停止读取。getline(cin, s)适合读可能包含空格的整行字符串。C风格的gets()/fgets()在老教材里常见但gets()在C11标准里已经被移除线上OJ的编译环境很可能不兼容。东华OJ的基础题如果是读单个单词cin是最稳的如果题目描述里说“输入一行字符串”且没有特意说明不含空格那你就得用getline处理。这里有个非常经典的坑如果你先用cin n读入一个整数再想用getline读入字符串会发现getline直接读到了一个空行。原因是cin n会在输入缓冲区里留下一个换行符而getline遇到换行符就结束了。解决办法有两种用cin.ignore()把那个残留的换行符清掉或者统一用getline读入所有数据再自己解析。我当初在这个小细节上卡了很久后来才知道OJ题里每类输入都用什么方式处理是有系统经验的。3. 三种判定思路从暴力的外围走向高效的核3.1 双向遍历最基础也最稳妥的思路判断回文最直观的做法就是用两个下标分别指向字符串的开头和结尾然后向中间靠拢逐一比较字符#include iostream #include string using namespace std; int main() { string s; cin s; int left 0, right s.length() - 1; bool isPalindrome true; while (left right) { if (s[left] ! s[right]) { isPalindrome false; break; } left; right--; } if (isPalindrome) { cout YES endl; } else { cout NO endl; } return 0; }这段代码的复杂度是 O(n)只需要遍历半个字符串空间复杂度 O(1)。这也是面试和OJ判题中最受欢迎的方案因为它只用一个循环就同时处理了奇数和偶数长度的情况不需要额外判断字符串长度的奇偶。对于长度为偶数的字符串比如abbaleft和right会在中间相遇循环自然结束。对于奇数长度的字符串比如abcba中间字符是c它不需要和谁比较循环会在left right时退出。这个边界逻辑非常干净没什么好担心的。3.2 反转字符串比较一行代码的思路但性能略逊另一种思路是构造原字符串的反转副本然后直接比较二者是否相等#include iostream #include string #include algorithm using namespace std; int main() { string s; cin s; string reversed_s s; reverse(reversed_s.begin(), reversed_s.end()); if (s reversed_s) { cout YES endl; } else { cout NO endl; } return 0; }这段代码更简洁也更容易理解适合刚学C string类的读者。但它需要额外开辟一份字符串内存时间复杂度 O(n)空间复杂度也是 O(n)。在OJ基础题里这个开销无伤大雅因为在数据量很小的情况下不会造成任何性能压力。但如果后续你想在更复杂的场景里使用回文判断比如判断一个超长文本的子串是不是回文反转法就不够看了。另外一点使用reverse需要包含algorithm头文件。有些老旧的OJ环境可以不用显式引入但在现代C编译环境下不写头文件就调用标准库函数是过不了编译的。3.3 递归思路理解价值大于实用价值递归判断回文的思路也很经典比较首尾字符是否相同如果相同则递归判断去掉首尾字符后的子串是否回文#include iostream #include string using namespace std; bool isPalindrome(const string s, int left, int right) { if (left right) { return true; } if (s[left] ! s[right]) { return false; } return isPalindrome(s, left 1, right - 1); } int main() { string s; cin s; if (isPalindrome(s, 0, s.length() - 1)) { cout YES endl; } else { cout NO endl; } return 0; }这段代码的逻辑很优雅但递归有函数调用开销而且在递归层级过深时可能导致栈溢出。对于OJ基础题来说你用它也能过裁判不会给你这么大的数据让它爆栈。但如果在真实工程项目中我不建议你用递归做回文判定因为同样的逻辑用循环写更省资源也更直观。递归版本的唯一好处是它能帮助你建立“分而治之”的思维方式这在后续学习二分搜索、归并排序、树的遍历时都非常重要。4. 从WA到AC我在这道题上踩过的三个坑4.1 第一次WA被忽略的输入残留坑我第一次写这题的时候代码逻辑是对的用的是双向遍历法。本地测试了abcba、abba、hello、a全都没有问题自信满满地提交结果弹出来一个 WA。当时我整个人是懵的。后来我冷静下来检查代码和题目的匹配度发现题目里很可能是“一行字符串”而我的cin s只读到了第一个单词。如果输入是hello world我的程序读入的只有hello它判断hello不是回文输出NO但答案应该是判断完整的hello world是否为回文。修复方案是如果原题输入可能包含空格就把cin s改成getline(cin, s)如果原题明确说明不含空格那cin就够了。通过这一次WA我养成了一个习惯先确认输入边界再动手写代码。这句话后来帮我避免了很多无意义的提交。4.2 数据范围用int还是size_t是个问题另一个注意点在于字符串的下标类型。s.length()返回的是size_t类型这是一个无符号整数。如果你写int right s.length() - 1;当字符串为空时s.length() - 1是一个巨大的正数而不是 -1。虽然这道题几乎不可能给空字符串但在某些变种题里如果允许空串输入你的程序就会在循环边界上发生无法预料的错误。稳妥的写法是int left 0; int right (int)s.length() - 1;做了显式类型转换然后在进入循环前检查一下s.empty()的情况。很多人觉得这是小题大做但OJ题就是这样的——你永远不知道评测机会扔给你什么边界数据。4.3 多余空格与换行看不见的字符串杀手第三种WA情况是输入里带了空格或不可见字符。有些题目的样例输入是这样的a b a你以为它测试的是aba这个字符串实际上程序处理的是a b a里面有空格如果你只用双指针判断s[1]是空格s[s.length()-2]也是空格两头同时是空格判断结果可能是回文但如果空格在不对称的位置判断就会出错。处理办法是在判断之前先把字符串里的空格、换行、Tab等无效字符过滤掉再执行回文判断。这其实也是很多企业面试题的真实考察点东华OJ的部分回文题正是这类变种。#include iostream #include string using namespace std; string removeSpace(const string s) { string result; for (char c : s) { if (!isspace(c)) { // 如果需要忽略大小写可以在这里统一转成小写 result tolower(c); } } return result; }整合到主程序里先调用removeSpace处理输入再走双指针判断基本能应对所有带空格的回文判定题。5. 完整AC代码拿过去就能交的版本最终我在东华OJ上通过的版本是这样的。它在逻辑上覆盖了空串、单字符、奇数长度、偶数长度、带空格输入这些情况并且考虑了可读性注释写得比较详细#include iostream #include string #include cctype using namespace std; bool isPalindrome(const string s) { int left 0; int right (int)s.length() - 1; while (left right) { // 忽略非字母数字字符如果需要 if (!isalnum(s[left])) { left; continue; } if (!isalnum(s[right])) { right--; continue; } // 忽略大小写 if (tolower(s[left]) ! tolower(s[right])) { return false; } left; right--; } return true; } int main() { string s; getline(cin, s); if (isPalindrome(s)) { cout YES endl; } else { cout NO endl; } return 0; }这是我个人用起来最顺手的一个版本。它比最简版多做了两件事——跳过非字母数字字符、忽略大小写。这两件事在题目没有要求时是多余的但有一说一这个版本几乎能适配所有变种回文题。如果你担心提交后被判定格式错误那就把isalnum和tolower相关的逻辑去掉回到第3节那段最基础的写法。我在实际OJ提交中这个带格式清理的版本在东华OJ的“回文问题”里过了在LeetCode的“验证回文串”里也过了属于一次学会、终身复用的那种模板。6. 举一反三回文问题的五种变体与C实现要点6.1 变体一回文子串计数题目不再是“判断整个字符串是否回文”而是让你统计一个字符串里有多少个回文子串。这个问题可以从每个字符或每两个字符的中点向两边扩展用“中心扩展法”解决复杂度 O(n²)。C实现里要注意substr的使用频率不要在主循环里频繁构造子串否则会被大数据卡住。6.2 变体二最长回文子串这是回文问题中最经典的进阶题。暴力解法是枚举所有子串复杂度 O(n³)在OJ里基本不可能通过。如果想拿满分你需要使用中心扩展法 O(n²)或者更高级的 Manacher 算法 O(n)。Manacher 算法核心是在每个字符之间插入#符号把奇偶长度统一处理然后维护一个当前已知的最右回文边界。这个算法在竞赛中很常见但需要单独花时间理解。6.3 变体三判断链表是否为回文在数据结构的题目里链表回文的判断也非常常见难点在于链表只能单向遍历不支持随机访问。常见的解法是用快慢指针找到链表中点然后把后半段链表反转再和前半段比较。这个思路把空间复杂度降到 O(1)也是很多大厂面试官喜欢考察的重点。6.4 变体四最多删一个字符后是否为回文LeetCode 680 就是这类题目。解法核心是双指针当第一次发现左右字符不相等时尝试分别跳过左侧一个字符或右侧一个字符然后判断剩余子串是否为回文。这个题目很好地考察了“回文判断分支取舍”的思维比单纯判断回文的难度高了一个层次。C实现时要注意删除字符时的边界条件比如跳过左侧字符后新的子串区间是[left1, right]跳过右侧字符时是[left, right-1]。6.5 变体五回文对LeetCode 336 是回文问题里的大Boss题目给你一组单词要求找出所有能拼接成回文的单词对。解法需要用到哈希表、前缀后缀的回文判断复杂度分析也比较复杂。这个题更适合在系统学习过字符串算法之后去挑战入门阶段先不着急。6.6 C实现回文问题的通用注意事项无论做上面哪一种变体有几点C的编码习惯是通用的我整理如下关注点建议原因字符串为空判断s.empty()不要直接用s[0]访问空串下标是未定义行为下标类型显式转换为int后再做减法size_t无符号溢出问题非常隐蔽大小写处理用tolower/toupper手写ASCII偏移不通用、不优雅字符过滤用isalnum判断字母数字手动判断中英文标点容易漏头文件cctype、string、algorithm按需包含漏头文件在严格编译下会CE换行输出优先用endl或\n统一风格输出格式是OJ判定的硬性指标7. 在Visual Studio Code里跑通本地测试的隐藏配置很多人刷OJ是直接在网页上提交代码的但总有一些同学习惯在本地先跑一遍测试再提交。如果你用的是 VS Code 配置 C 环境我提醒你几个容易出错的小地方。第一个是编译命令。你可以在终端里用g -stdc17 -Wall -Wextra -O2 -o solution solution.cpp-Wall和-Wextra会显示所有警告比如未使用变量、比较类型不一致等问题。强制自己消除警告是减少OJ WA概率的好习惯。-O2是开优化本地跑大数据时更接近OJ的判定速度。第二个是launch.json 和 tasks.json。VS Code 里按 F5 调试 C 程序需要配置好调试器路径和编译任务。如果一直配置不成功并不一定是代码有问题而是编译任务里的“参数”字段没写好。最简单的做法是先装好 C/C 扩展在命令面板执行“C/C: Edit Configurations (UI)”把编译器路径指到你的g.exe所在目录。第三个是标准输入问题。在 VS Code 的终端里运行程序时如果程序使用了getline(cin, s)你需要手动输入字符串再按回车。如果输入包含空格终端输入和OJ读入的行为是一致的。但如果你的测试数据比较多建议直接重定向文件输入./solution input.txt这样cin会从input.txt读取数据和你调试的输出分开效率更高。8. 从这道题延伸出去的思维模型回文背后的“双指针”思想回文问题看起来是个字符串问题但它背后的核心是“双指针”思想。这种思想在算法题里无处不在有序数组两数之和用双指针从两端往中间逼近快排的 partition 过程也是两个指针从两端向中间扫描链表找环、找中点是快慢双指针的经典应用滑动窗口的左右边界本质上也是两个指针协作所以别小瞧东华OJ的这道基础回文题。吃透它的双指针写法你会发现在其他题目里相同的思想反复出现。有一个很直观的类比双指针就像两个探路的人一个站在队伍头部一个站在队伍尾部他们同时向中间走每走一步就核对一下手中的信息是否一致。如果整个过程中任何一步不一致就说明队伍从头到尾的安排有问题。这种“从两端向中间汇聚”的思路和很多问题的解决逻辑是相通的。比如判断一个括号序列是否合法、验证栈的压入弹出序列是否匹配、检查一个数组在某个排序规则下是否对称……它们都用到左右夹逼的思想。所以我在很大程度上认为回文问题不是一个孤立的知识点。它更像一把钥匙打开的是算法思维训练里非常重要的一扇门。如果你能在一道“基础题”里看到这层东西那刷题就不只是“过题”了而是在不断积累自己的算法直觉。就我个人经验而言东华OJ基础题的价值恰恰在这里题目本身不炫技、不复杂但它们挑出来的都是最核心的考点。把每一道基础题的思路吃透再去做LeetCode的中等、困难题你会发现自己其实是在用已经练熟的基本功去拆解更复杂的场景而不是每次都从零开始。
返回列表