Hot100刷到第六天正好撞上字符串这一组。说实话刷数组和链表的时候我感觉还挺顺畅套路也比较固定但字符串题一上来就给了我一个下马威——同样是线性结构字符串题对边界处理、编码理解和语言API的敏感度要求高得多。第六天我给自己定的计划很老实不追求一口气刷完所有相关题目而是把Hot100里典型的字符串场景都过一遍尤其是逆序、回文、转换、分割这几类每道题都逼自己回答一个问题它到底在考什么。这篇文章适合两类人。一类是正在刷Hot100、被字符串题反复折磨的选手希望能帮你把“散装知识点”串成几条能复用的主线另一类是平时写业务代码、偶尔要处理字符串转换和校验的开发者里面不少工程侧的坑可能你也踩过。我不打算把每道题的完整题解贴一遍那没有意义我重点讲思路、讲取舍、讲那些网上题解里不会明说的细节。1. 字符串题型的本质与Hot100覆盖范围1.1 字符串题其实就是数组题的“加强版”很多人把字符串当成一种特殊的基本类型刷题时才发现它本质上还是字符的线性容器。数组里考你的遍历、双指针、哈希、动态规划字符串里全会再考一遍但它还额外增加了三样麻烦不可变性、字符编码、语言API差异。C语言里字符串就是char数组以\0结尾你要手动管理内存Java里String是不可变对象任何修改都产生新对象Python里字符串是不可变Unicode序列切片和join语义和C完全不一样。同一个“字符串逆序”题目用C写要考虑指针和终止符用Java写要考虑StringBuilder用Python写一行切片就结束了。我在刷Hot100到这个专题时最大的感悟是字符串题不只是在考算法更是在考你对一门语言底层模型的理解。如果你只会在IDE里写str.split()和str.contains()而不清楚它们背后的遍历逻辑一旦面试官追问“为什么复杂度是O(n)”就会露怯。1.2 Hot100里字符串题到底覆盖了哪些场景把Hot100里和字符串强相关的题目拉出来看大致分这么几类逆序翻转、回文处理、字符串转换、子串匹配与滑动窗口、以及少量涉及动态规划的题目。为了直观我列了一个表题目类型典型题目核心考点逆序/翻转344. 反转字符串、151. 反转字符串中的单词双指针、原地操作回文处理5. 最长回文子串、647. 回文子串中心扩展、DP转换/校验8. 字符串转换整数、165. 比较版本号边界处理、溢出判断子串匹配/窗口3. 无重复字符的最长子串、76. 最小覆盖子串滑动窗口、哈希动态规划139. 单词拆分、72. 编辑距离状态转移、枚举子串前缀匹配14. 最长公共前缀纵向/横向比较其实Hot100里字符串题的数量不算特别多但每一道几乎都是经典中的经典。比如“无重复字符的最长子串”表面是字符串题核心却是滑动窗口“编辑距离”表面是两个串的比较内核是二维DP。把这些题目串起来你会发现字符串是一个极好的“综合题型容器”它把数组、哈希、双指针、DP全部封装在了一层文本外衣下面。2. 字符串题目的三板斧双指针、哈希、动态规划2.1 双指针逆序、分割和原地修改的最佳拍档字符串逆序最经典的写法就是双指针。左指针从头走右指针从尾走两边交换直到指针相遇。很多人第一次写的时候会把循环条件写成while (left right)还是while (left right)犹豫半天——其实就够了中间那个字符不需要跟它自己交换。这里有一个很容易被忽略的点C语言里字符串逆序千万不能把\0换到前面去。我见过有人把strlen(s)的结果直接当成右指针起点结果把结束符也参与了交换输出直接乱码。正确做法是right strlen(s) - 1。分割字符串同样靠双指针思想。热词里有一句“C语言将一个字符串按照里面的空格分开”工程上大家直接用strtok或split但刷题时你要理解底层用一个指针标记单词起点另一个指针向后扫描遇到空格或结束符就切出一个子串。这里最大的坑是连续多个空格处理办法是跳过所有分隔符再开始记录新单词。2.2 哈希表与“是否包含”问题字符串题目里“判断是否包含”“找子串出现位置”“判断两个字符串是否互为字母异位词”这类问题绝大多数可以用哈希表优化。字母异位词的核心思路是统计每个字符出现的次数C语言可以用长度为26的数组当哈希表Java和Python用HashMap或Counter。热词里有一串“js判断字符串是否包含”“oracle判断字符串是否包含某个字符串”“java字符串与字符串相似度”——这些在工程里都有现成API但刷题时要明白库函数的底层逻辑。strstr、indexOf、includes、in实现方式基本不是朴素遍历就是KMP。平时用库函数不代表算法不重要面试官考的就是你能否在“没有库函数”的环境下手写出等价逻辑。再看“无重复字符的最长子串”这类高频题核心是滑动窗口维护一个“窗口内字符不重复”的状态右指针不断扩展左指针在遇到重复字符时右移。很多题解说“用HashMap记录字符最后一次出现的位置”其实最常见的写法是记录索引然后更新左边界为max(left, lastIndex[ch] 1)。这里容易写错的地方是遇到重复字符时不能直接把left跳到lastIndex[ch] 1吗理论上可以但前提是你只在窗口范围内更新。如果你记录的lastIndex[ch]是一个早已离开窗口的旧位置直接跳就会导致左边界向左移动逻辑就崩了。2.3 动态规划回文子串和编辑距离是两大代表字符串DP题有一个非常通用的子问题定义方式dp[i][j]表示从第i个字符到第j个字符是否满足某个性质。以“回文子串”为例dp[i][j]为true表示s[i..j]是一个回文串状态转移是dp[i][j] (s[i] s[j]) (j - i 2 || dp[i1][j-1])这个递推公式看着简单写代码却特别容易错因为遍历顺序有讲究。如果你按i从0到n、j从i到n的顺序填表dp[i1][j-1]可能还没被计算过结果全错。正确做法是按子串长度从小到大遍历或者让i从大到小递减。热词里有一句“alice得到了一个字符串s。她觉得回文子串十分的interesting”这类题目经常出现在牛客和竞赛里核心解法不是中心扩展就是DP两者时间复杂度都是O(n^2)但DP更容易理解、更容易扩展。“编辑距离”则是两个字符串DP的代表dp[i][j]表示把word1的前i个字符转换成word2的前j个字符所需的最少操作次数状态转移考虑增、删、改三种操作。这种题的难点不是写递推而是理解为什么dp[0][j]j、dp[i][0]i这些初始化是合理的。我建议刷字符串DP题时动手画一张二维表把转移过程走一遍比反复看题解高效得多。3. 典型题目拆解与代码实现3.1 最长回文子串中心扩展法要写到条件反射最长回文子串是Hot100里的经典题。暴力解法是枚举所有子串再判断回文复杂度O(n^3)基本一定会超时。中心扩展法复杂度O(n^2)是笔试面试最稳的写法。思路很简单每个字符和每两个相邻字符中间的位置都可能作为回文中心分别向两边扩展直到左右字符不同或越界为止。def longestPalindrome(s: str) - str: def expand(l: int, r: int) - str: while l 0 and r len(s) and s[l] s[r]: l - 1 r 1 return s[l 1:r] res for i in range(len(s)): s1 expand(i, i) # 奇数长度回文中心是一个字符 s2 expand(i, i 1) # 偶数长度回文中心是两个字符之间 if len(s1) len(res): res s1 if len(s2) len(res): res s2 return res为什么要有两种中心因为“aba”的中心是单个字符“b”“abba”的中心却是两个字符“bb”之间的位置。只考虑一种中心一定会漏掉一半的情况。如果你追求极致性能可以学Manacher算法把复杂度降到O(n)但坦率讲笔试时能把中心扩展写熟练就够了Manacher属于“知道思路但没必要默写”的范畴。解法时间复杂度空间复杂度面试推荐度暴力枚举O(n^3)O(1)不推荐基本超时中心扩展O(n^2)O(1)推荐容易写对动态规划O(n^2)O(n^2)推荐适合追问时迁移ManacherO(n)O(n)有余力再掌握3.2 字符串转整数溢出和非法输入一个都别漏“字符串转整数”是每个刷题人都会遇到的一道题。对应LeetCode第8题考的无非是跳过前导空格、处理正负号、逐位累加、判断溢出。热词里那一长串“sqlserver字符串转数字”“mysql将字符串转为日期”“oracle过滤不可转为数字的字符串”“abap判断字符串是否是数字”说到底都是同一个问题在工程侧的投影——数据清洗时你永远不知道输入框里会有什么妖魔鬼怪。核心实现逻辑我一般这样写public int myAtoi(String s) { if (s null || s.length() 0) return 0; int i 0, n s.length(); while (i n s.charAt(i) ) i; int sign 1; if (i n (s.charAt(i) || s.charAt(i) -)) { sign s.charAt(i) - ? -1 : 1; i; } int result 0; while (i n Character.isDigit(s.charAt(i))) { int digit s.charAt(i) - 0; if (result (Integer.MAX_VALUE - digit) / 10) { return sign 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; } result result * 10 digit; i; } return result * sign; }很多新手会把溢出判断写成result * 10 digit Integer.MAX_VALUE问题是result * 10本身就可能溢出Java里直接爆掉。更稳妥的写法是提前判断result (Integer.MAX_VALUE - digit) / 10。这个判断的意思是“如果result已经大到即使只加一个digit也放不下那就直接返回极限值”。这里有一个考场高发失误——忘记考虑只有“-”号或只有“”号的情况。返回值应该是0而不是报错。3.3 反转字符串中的单词一个综合练习题这道题把逆序和分割两个知识点结合在了一起。最直观的思路是先用空格分割再反转单词顺序最后拼接。工程里Python三四行搞定def reverseWords(s: str) - str: return .join(reversed(s.split()))但如果是C语言就要实打实处理指针和内存。热词里有“C语言将一个字符串按照里面的空格分开”说明很多人卡在了手写分割上。这里贴一个可供参考的C风格实现#include stdio.h #include string.h #include stdlib.h void reverse(char *s, int l, int r) { while (l r) { char tmp s[l]; s[l] s[r]; s[r] tmp; l; r--; } } void trimSpaces(char *s) { int n strlen(s); int write 0; for (int read 0; read n; read) { if (s[read] ! ) { if (write 0) s[write] ; while (read n s[read] ! ) s[write] s[read]; } } s[write] \0; } void reverseWords(char *s) { trimSpaces(s); int len strlen(s); reverse(s, 0, len - 1); int start 0; for (int i 0; i len; i) { if (s[i] || s[i] \0) { reverse(s, start, i - 1); start i 1; } } }先去除首尾和中间多余空格再把整个字符串反转最后逐个单词反转回来这是经典的两段式反转法。很多人在写reverseWords时忘记先处理空格导致连续空格的地方出现空串输出就多了一堆空格。4. 各语言字符串处理的坑与实操记录4.1 C/C的字符串陷阱数组、指针、内存泄漏C语言字符串以\0结尾这个常识人人知道但实际操作中还是容易踩坑。比如用strcpy拷贝字符串时目标数组的空间必须至少是strlen(src) 1多出来的1就是结束符的位置。热词里“mfc字符串内存泄漏”就是这类问题的典型代表——在循环里反复new char[]却忘记delete[]时间一长内存就爆了。C里我更喜欢直接用std::string它自带内存管理和长度信息安全很多。但C也有自己的坑std::string::c_str()返回的是const char*直接强转成char*去修改是非法的“指针数组存放字符串”的时候char *arr[]和char arr[][N]的布局完全不同前者每个字符串独立存储后者是一块连续内存。字符串比较也是一个经典坎。C语言里比较两个字符串内容要用strcmp直接用比较的是指针地址几乎永远不相等。C的std::string重载了却让很多人回到C语言时习惯性地写if (s1 s2)然后一脸懵。4.2 Java、JS、Python的API与编码问题Java的String不可变这对刷题影响非常大。频繁拼接字符串时最好用StringBuilder而不是否则每次拼接都产生新对象。热词里的“java字符串后边.getbytes()”也是一个高频用法但getBytes()默认使用平台编码跨环境容易乱码建议指定StandardCharsets.UTF_8。JavaScript的模板字符串和字符串方法也是日常工作里离不开的。模板字符串用反引号和${}插值写SQL片段或拼接HTML时很好用。正则提取的话热词里“提取出中间的数字及#符号后的字符串”这种描述只要学会match配合捕获组就能搞定。Python里则注意split()和split( )的区别前者会智能处理连续空格后者不会。场景C/CJavaPythonJavaScript获取长度strlen/length()length()len().length判断包含strstr/findcontainsinincludes内容比较strcmp/equals/!取子串strncpy/substrsubstring[a:b]slice转数字atoi/stoiInteger.parseIntint()Number()分割strtok/ 手写splitsplitsplit大小写tolowertoLowerCaselower()toLowerCase4.3 SQL和脚本场景下字符串转换的实践经验刷题刷多了你会发现字符串转数字、判断是否为数字这些操作在业务开发里同样高频出现。SQL Server里转数字常用CAST(column AS INT)但遇到脏数据会直接报错于是有了TRY_CAST这种版本转换失败返回NULL而不是报错。MySQL里字符串转日期用STR_TO_DATE(str, %Y-%m-%d)但格式串写错是日常翻车点。热词里那句“oracle过滤不可转为数字的字符串”很有代表性。Oracle没有直接的ISNUMERIC常见做法是用REGEXP_LIKE(column, ^[-]?[0-9]$)来筛选纯数字行或者用TRANSLATE把数字替换为空再判断。DB2里也有类似的判断数字字符串的函数。这类“判数字”场景看起来简单实际正则写不对就会漏掉“1”“-2”“1.0”这些合法数字。ABAP里判断字符串是否是数字也有专门写法ODBC连接字符串里更是充满了分号和转义稍不注意就配错。我的建议是刷题之余把你工作里遇到过的每一种“字符串转数字/过滤脏数据”的方案整理成速查表。字符串这个专题的价值不在题目本身而在它能帮你在算法题和工程实践之间搭一座桥。5. 刷题过程中最常见的坑与排查技巧5.1 边界条件清单写代码前先过一遍字符串题的边界条件通常比数组题更隐蔽因为除了索引你还得多考虑空串、空白字符和编码。我吃过的亏集中在这几类空字符串和只有空格的字符串处理结果完全不同但很多人在手写代码时根本不判断。单字符字符串的回文判断、反转操作容易让人写出越界访问。全相同字符串比如“aaaa”回文子串数量是最大的需要验证中心扩展的计数逻辑。长度为1万以上的长字符串递归方法容易爆栈。字符串中间夹着多个连续空格尤其要小心。我给自己定了一个规矩每道字符串题写完先跑5个脑内用例——空串、单字符、首尾带空格、多个连续空格、超大输入。不要小看这几秒钟它能帮你拦截至少一半的提交错误。5.2 运行超时的排查思路字符串题超时绝大多数不是算法问题而是复杂度选错了。比如“无重复字符的最长子串”如果你用两重循环枚举所有子串然后在循环里再用indexOf判断重复复杂度直接O(n^3)用例一大就跑不动。这种时候先别急着优化代码回到方法选择上——是不是该用滑动窗口了Java里还有一个防不胜防的坑在循环中用String 拼接字符串。我见过有人用字符串累积的方式构造一个超长结果循环几千次之后性能急剧下降。改用StringBuilder之后耗时从几秒降到了几十毫秒。字符串题很特殊它不只考你算法还考你对语言底层特性的理解。5.3 调试技巧打印中间状态比瞪眼有效字符串题出bug之后我最推荐的方法是打印中间变量。比如做回文DP时把dp二维数组打出来一眼就能看出是不是遍历顺序写错了。做滑动窗口时打印窗口的左右指针和当前哈希状态能快速定位是左边界移动错了还是右边界漏了。另一个很实用的方法是写一个小的辅助函数比如判断两个字符串是否互为异位词、判断某个区间是否回文单独抽出来测。很多字符串题的bug并不复杂只是混在大代码里容易被忽略。辅助函数独立出来后问题往往一下就暴露了。也可以准备几组“对拍”样例。自己构造一个小型随机测试器用暴力解法当参考答案和你的优化解法对比输出。这在排查“反转字符串中的单词”这种题目时特别好用因为空格处理很容易出现偏差。最后分享一个我自己一直沿用的笨办法字符串题刷完后不要急着看下一题而是把这道题的核心操作翻译成你工作里最常用的语言再写一遍。比如我用Java刷完atoi顺手把项目里的数据清洗逻辑重构了一下原来要写好多次try-catch的地方现在直接抽成工具类。字符串这种东西价值的落点往往不在题本身而在你借它把语言基础、边界思维、编码习惯重新捋了一遍。这个专题刷完比多做十道新题都值。