0. 写在前面:为什么字符串是竞赛题里的“隐形炸弹”
先说个我自己的观察。每次带学弟学妹打蓝桥杯、校内赛或者力扣周赛,十道题里至少有六道绕不开字符串操作。很多人觉得字符串很简单,不就是 String 拼接、substring 截取、split 切分嘛,可一到数据量大的时候,“超出时间限制”“内存溢出”瞬间就把整场心态搞崩。你以为是算法没想出来,其实很多时候是字符串的处理姿势不对——String 的不可变性、substring 的底层实现、split 的正则陷阱,每一个都是隐藏的坑。
这篇东西就是写给准备竞赛、刷题、面试前突击的 Java 选手看的。我会把竞赛中最常用到的字符串操作手段、容易踩坑的细节,以及可以直接背下来的模板代码整理成一份实操手册。保证不堆理论,每一条都是可以直接在 IDE 里跑、在比赛里用的东西。
标题里写着“正确姿势”和“常用模板”,那我就顺着这两个词展开:前半部分讲思路和细节,后半部分给代码。你照着敲一遍,基本就吃透了。
1. 竞赛场景下字符串问题的整体设计思路
1.1 先搞清楚:竞赛中的字符串题到底在考什么
字符串类的题从来不单独考察“你会不会调用 API”,它真正考的是三件事:
- 字符串不可变性带来的性能陷阱。String 在 Java 里是不可变对象,每一次拼接、替换、截取都可能产生新对象。小数据量没问题,一旦循环里反复拼接,就会制造大量中间垃圾对象,触发 GC,甚至直接 OutOfMemoryError。热词里那个
java: outofmemoryerror: insufficient memory报错,我在刷题群里见过无数次,十有八九是循环拼接 String 搞出来的。 - 字符处理与边界条件。竞赛题非常喜欢在边界上做文章,比如空字符串、单个字符、全相同字符、包含大小写/数字/特殊符号的混合串。如果平时不注意这些 case,交上去就是一个 WA(Wrong Answer)。
- 算法模型与字符串特性的结合。比如回文串判断要想到双指针或者 Manacher,子串匹配要想到 KMP,词频统计要想到哈希表。字符串只是载体,真正的考点还是算法。
想清楚这三点,你就不会再去死记硬背 API,而是会主动思考“我这次用的是不是最优的数据结构”“这个操作会不会产生额外对象”“这种写法能支撑多大的数据范围”。
1.2 解题时的“两问一查”决策法
我在实际做题时,拿到字符串题会先问自己两个问题,再查一个东西,这套流程能帮你快速选择正确姿势。
第一问:数据范围多大? 题目里如果写 n <= 10^5,那 O(n^2) 的暴力解法基本就危险了;如果是 n <= 10^3,那随便用 substring、split 都没太大问题。数据范围直接决定你能不能任性。
第二问:要不要修改原字符串? 如果需要频繁修改,就别用 String,直接用 StringBuilder 或者 char[]。这个决策直接影响性能。
一查:查 API 的返回值。 很多人会搞混 String 的 replace、replaceAll、replaceFirst 返回的是新字符串,原串并不会变。竞赛里你要是忘了重新赋值,后面全是 bug。
2. 核心细节解析:String、StringBuilder、字符数组的选型与实操要点
2.1 String 的“不可变”到底意味着什么
先做一个最直观的实验。你在循环里写:
java复制String s = "";
for (int i = 0; i < 100000; i++) {
s += "a";
}
这段代码跑起来会非常慢,甚至内存直接爆掉。为什么?因为 String 是 final 类,底层是 private final byte[] value,一旦创建就不能改。每次 s += "a",JVM 都会新建一个 String 对象,把旧内容拷贝过去,再追加新字符。循环十万次就是十万次数组复制、十万个对象产生。这还不算完,那些旧对象会留在堆里等 GC,GC 一跑,整个程序卡顿。竞赛时限一秒两秒的,这种写法直接送走。
这个例子也解释了为什么热词里会出现 java: outofmemoryerror——不是内存真的不够总容量,而是短时间创建了大量垃圾对象,GC 来不及回收,堆内存被打满。所以竞赛中处理字符串的第一原则:循环内禁止使用 String 直接拼接。
2.2 StringBuilder:竞赛中最高频的字符串工具
StringBuilder 是可变的字符序列,底层是一个可以自动扩容的 byte[]。你调用 append() 是在数组末尾追加元素,而不是创建新对象,性能差异是数量级的。
我在竞赛中会无脑选 StringBuilder,原因很简单:它足够快,API 也够用。StringBuffer 是线程安全的,性能差一些,但竞赛单线程环境下根本不需要线程安全,StringBuilder 就够了。
常用的模板我总结成一套,你直接背:
java复制// 1. 创建
StringBuilder sb = new StringBuilder(); // 空串
StringBuilder sb2 = new StringBuilder("init"); // 初始内容
// 2. 追加
sb.append('a'); // char
sb.append("abc"); // String
sb.append(123); // int,会转成字符串形式
sb.append(new int[]{1,2}); // 数组会变成 [I@xxxx,注意这是个坑!
// 3. 删除和插入
sb.deleteCharAt(3); // 删除指定位置
sb.delete(2, 5); // 删除 [2,5) 区间
sb.insert(0, "prefix"); // 在指定位置插入
// 4. 翻转(回文串题的救命用法)
sb.reverse();
// 5. 取子串
String sub = sb.substring(1, 3); // 返回 String,不是 StringBuilder
// 6. 转回 String
String result = sb.toString();
// 7. 替换单个字符(比 String 灵活多了)
sb.setCharAt(0, 'X');
这里有个特别容易错的点:substring 返回的是 String,不是 StringBuilder。你要是想把结果再 append,得先 toString 回去,或者直接写 sb.append(sb.substring(1,3)),不要搞反。
2.3 字符数组 char[]:适合什么场景
有些场景 StringBuilder 也不好使,最典型的就是“需要频繁随机修改字符”的题,例如翻转字符串中的元音字母、移动零、按规则交换字符。这时候直接用 char[] 更顺手,因为你可以像操作数组一样操作每个位置。而且刷题时遇到“单词翻转”“括号匹配”“双指针扫描字符串”这类题,toCharArray() 是标配。
java复制String s = "hello world";
char[] arr = s.toCharArray();
// 双指针原地交换
int left = 0, right = arr.length - 1;
while (left < right) {
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
String reversed = new String(arr);
注意,new String(arr) 这一步是必要的,因为题目要求返回字符串。直接把 char[] 打回去是错的,很多编译器甚至会直接报类型错误。
3. 竞赛必背:高频字符串题型模板
3.1 回文串相关:从双指针到动态规划
回文串是竞赛的基础题,也是很多进阶题型的底层模块。我见过最朴素的错误写法是一上来就 reverse() 然后 equals 比较,这个方法在判断“整个字符串是否回文”的简单场景下没问题,一旦涉及子串、最长的回文子串,它就完全不够用了。
场景一:判断一个字符串是否是回文串
java复制public boolean isPalindrome(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
这个模板看起来简单,但要注意一个点:如果题目要求“忽略大小写、忽略非字母数字字符”,比如力扣的 125 题,你需要先做预处理或加条件判断,不要直接比较字符。
场景二:最长回文子串
经典解法有两种:中心扩展法和动态规划。中心扩展法的实现简单,空间复杂度 O(1),竞赛里够用。核心思路是把每一个字符和每两个相邻字符当作回文中心向外扩展。
java复制public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, maxLen = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i); // 奇数长度回文
int len2 = expandAroundCenter(s, i, i + 1); // 偶数长度回文
int len = Math.max(len1, len2);
if (len > maxLen) {
start = i - (len - 1) / 2;
maxLen = len;
}
}
return s.substring(start, start + maxLen);
}
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1; // 注意:这里要减 1
}
中心扩展最常出错的地方就是 right - left - 1。循环结束后 left 和 right 都已经多走了一步,所以实际回文长度是 right - left - 1,不是 right - left + 1,也不是 right - left。这个我帮人改过很多次代码,都是在边界上翻车。
3.2 子串匹配:KMP 模板与 next 数组
竞赛里一提到“判断字符串 A 是否包含字符串 B”“在字符串中找模式串出现的位置”,很多人的第一反应是 indexOf()。这个 API 在普通场景下确实够用,底层是暴力匹配,但一旦算法题要求你的复杂度可控,或者题目明确说了 n 在 10^5 以上,KMP 就是你需要拿出来的东西。
KMP 的核心是 next 数组,它的含义是“模式串中,以 i 结尾的字符形成的子串,最长相同前后缀的长度”。我直接给一份可以直接背的模板:
java复制public int strStr(String haystack, String needle) {
if (needle == null || needle.length() == 0) return 0;
int n = haystack.length(), m = needle.length();
// 构建 next 数组
int[] next = new int[m];
int j = 0;
for (int i = 1; i < m; i++) {
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if (needle.charAt(i) == needle.charAt(j)) {
j++;
}
next[i] = j;
}
// 匹配过程
j = 0;
for (int i = 0; i < n; i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1; // 返回第一次匹配的起始下标
}
}
return -1;
}
这个模板的难点在于理解 while 回退那里。每次字符不匹配时,j 要回退到 next[j-1],而不是清零,这样才能利用已匹配的前缀信息。你要是第一次写 KMP,建议用 "ababc" 这种模式串手动模拟一遍 next 数组的构建过程,一通百通。
3.3 字符串哈希:快速判断子串相等的利器
竞赛里有一类题,需要频繁比较两个子串是否相等。比如多次询问一个字符串任意两个区间的子串是否相同。你用 substring() + equals() 做,每次都是 O(n) 的复杂度,问几千次直接超时。这时候就要用字符串哈希,把字符串映射成一个整数(通常是 long),比较两个子串是否相等就变成比较两个整数是否相等,单次判断 O(1)。
常用的哈希方法是滚动哈希(也叫 Rabin-Karp 哈希)。做法是预处理前缀哈希和幂数组:
java复制class StringHash {
private static final long BASE = 131L;
private long[] hash;
private long[] power;
private int n;
public StringHash(String s) {
n = s.length();
hash = new long[n + 1];
power = new long[n + 1];
power[0] = 1;
for (int i = 0; i < n; i++) {
hash[i + 1] = hash[i] * BASE + s.charAt(i);
power[i + 1] = power[i] * BASE;
}
}
// 返回区间 [l, r) 的哈希值
public long getHash(int l, int r) {
return hash[r] - hash[l] * power[r - l];
}
}
这个模板的原理是把字符串当作一个 BASE 进制的数,hash[i] 表示前 i 个字符的哈希值。要取区间 [l, r) 的子串哈希,用 hash[r] - hash[l] * power[r-l] 把左侧多算的部分减掉。BASE 取 131 或 13331,通常能有效减少冲突。注意这里用的是 long 的自然溢出,因为 Java 的 long 溢出是自动 wrap 的,相当于对 2^64 取模。
使用场景很多:判断回文串(正着哈希和反着哈希比较)、查找最长重复子串、字符串匹配、分组字符串等。这是竞赛进阶的必备模板。
3.4 统计频率与分组:HashMap + 字符串的经典组合
有一类问题看起来是字符串题,实际考的是哈希表。比如“判断两个字符串是否互为字母异位词”(anagram)、“找出字符串中第一个出现次数最多的字符”、“对字符串数组按字符组成分组”。
这类题的套路非常固定:把字符串转成 char[],排序后作为 key,或者用 int[26] 统计每个字母出现次数,把这个数组转成字符串作为 key。最后再用 HashMap 去重或分组。
java复制// 判断 s 和 t 是否互为字母异位词
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
for (char c : t.toCharArray()) {
count[c - 'a']--;
}
for (int val : count) {
if (val != 0) return false;
}
return true;
}
注意 c - 'a' 这个操作只适用于纯小写字母。如果题目里包含大写字母、数字,就不能用 int[26],要么把数组扩大到 128 或 256,覆盖 ASCII 码范围,要么直接用 HashMap<Character, Integer>。竞赛里看清楚题目对字符集的要求,别想当然。
分组那类题也值得给个模板:
java复制public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] arr = s.toCharArray();
Arrays.sort(arr);
String key = new String(arr);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}
computeIfAbsent 是 Java 8 引入的方法,写起来比先判断 containsKey 再 get 再 put 简洁太多。竞赛中能少写几行就少写几行,省下的时间都是宝贵的。
4. 实操过程:从暴力到优化的字符串题完整推演
4.1 一个真实案例:找出字符串中所有字母异位词的起始下标
这道题非常典型,输出它的完整解题过程最能说明字符串题应该怎么想。题目是这样的:给定字符串 s 和 p,找出 s 中所有 p 的异位词的起始下标,返回下标组成的列表。
举个例子,s = "cbaebabacd",p = "abc",结果应该是 [0, 6],因为下标 0 的子串 "cba" 和下标 6 的子串 "bac" 都是 "abc" 的异位词。
第一步,暴力思路:枚举 s 中所有长度等于 p 的子串,对每个子串排序,再和排序后的 p 比较。排序复杂度是 O(k log k),枚举 N 个位置,总复杂度 O(N * k log k),当 N、k 都到 10^5 时直接超时。
第二步,优化思路:用固定大小的滑动窗口,窗口内维护一个字符计数数组,和 p 的计数数组不断比较。窗口向右移动一步,只需要更新左边滑出字符和右边滑入字符的计数,复杂度降到 O(N * 26),可以接受。
第三步,上代码:
java复制public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
if (s.length() < p.length()) return res;
int[] pCount = new int[26];
int[] sCount = new int[26];
for (char c : p.toCharArray()) {
pCount[c - 'a']++;
}
int k = p.length();
for (int i = 0; i < s.length(); i++) {
// 加入右边界
sCount[s.charAt(i) - 'a']++;
// 移除左边界,保持窗口大小为 k
if (i >= k) {
sCount[s.charAt(i - k) - 'a']--;
}
// 比较两个计数数组
if (Arrays.equals(pCount, sCount)) {
res.add(i - k + 1);
}
}
return res;
}
这个写法里,Arrays.equals(int[], int[]) 帮我们完成了数组比较,不用手动循环。每个窗口比较是 O(26),也就是常数时间,整体复杂度 O(N),特别适合竞赛。
4.2 案例延伸:Comparator 与 Lambda 在字符串排序中的实战
热搜词里出现了很多次 lambda、comparator.comparing、排序这类关键词,在字符串题里它们也真的很有用。比如给你一个字符串数组,要求按字符串长度从小到大排序,长度相同按字典序倒序。用 lambda 可以写得很清爽:
java复制String[] words = {"banana", "apple", "cherry", "date"};
Arrays.sort(words, (a, b) -> {
if (a.length() != b.length()) {
return a.length() - b.length();
}
return b.compareTo(a); // 字典序倒序
});
另一个更常见的场景是按某个规则取最大或最小字符串。比如要找字符串数组中字典序最大的字符串:
java复制String maxStr = Arrays.stream(words)
.max(Comparator.comparing(String::toString))
.orElse("");
不过说实话,竞赛中能用传统 for 循环解决的尽量用 for 循环,Stream 虽然简洁但有些裁判机上的 Java 版本可能不支持,或者性能稍微差一点。比赛求稳,我一般在 C 和 D 组题里才用 Stream,A、B 组可能刷速度时还是老老实实用 for。
4.3 从 BigInteger 到数字字符串转换
字符串题里还有一类绕不开的场景:超大整数的加减乘除。Java 的 long 最大是 9.22 * 10^18,一旦超过这个范围,数字就存不下了,但那不是字符串出现问题,而是要用大数处理。Java 内置 BigInteger 可以直接处理,但它性能很差、对象很重,竞赛题如果特意卡这一点,会直接超时。
这个时候就需要把大数当成字符串手动模拟竖式运算。比如“两个字符串表示的非负整数相加”:
java复制public String addStrings(String num1, String num2) {
StringBuilder sb = new StringBuilder();
int i = num1.length() - 1, j = num2.length() - 1, carry = 0;
while (i >= 0 || j >= 0 || carry > 0) {
int x = i >= 0 ? num1.charAt(i--) - '0' : 0;
int y = j >= 0 ? num2.charAt(j--) - '0' : 0;
int sum = x + y + carry;
carry = sum / 10;
sb.append(sum % 10);
}
return sb.reverse().toString();
}
注意最后必须要 reverse(),因为我们是低位开始追加的,存到 StringBuilder 里是倒着的。这个模板同样适用于大数相乘(用数组模拟)和二进制字符串相加(把 10 换成 2)。
5. 常见问题与排查技巧实录
5.1 字符串拼接导致性能灾难
这是我见过最多的性能问题,甚至很多工作多年的人也会犯。前面已经解释了原理,这里再给一个对比数据。
在本地跑 10 万次字符串拼接,用 String += 大概要几秒钟,用 StringBuilder 只需几毫秒,性能差距接近三个数量级。在竞赛里,这个差距直接决定你是 AC 还是 TLE。
注意:不要在循环体内使用
s += item,正确的做法是StringBuilder sb = new StringBuilder();然后在循环里sb.append(item),最后sb.toString()。
另外,String.format() 和 + 拼接在竞赛里也尽量少用。String.format 内部走的是 Formatter,开销很大;+ 在单次拼接时编译器会帮你优化成 StringBuilder,但循环里就危险了。
5.2 substring 的底层隐藏陷阱
Java 7 之前的版本中,substring() 的内部实现会直接复用原字符串的 char[],通过偏移量来标记子串。这意味着你截取一个小子串,底层却还持有整个大字符串的引用,导致大字符串无法被 GC 回收,大量这种操作就会出现内存溢出。
Java 7 之后改了实现,substring 会复制新数组,旧字符串可以被正常回收。但代价是每次 substring 都带来一次拷贝。竞赛时如果你频繁调用 substring 并且字符串很长,同样会有性能问题。
我个人的建议是:如果你只需要某个位置的字符或一段字符做比较,不要用 substring,而是用 charAt 配合双指针,或者直接用 indexOf + 比较。真正需要返回子串的时候才用 substring。 这样既能避免拷贝开销,又能避免内存上的坑。
5.3 split 的正则陷阱
String.split(String regex) 的参数是正则表达式,不是一个普通字符串。你如果直接用 s.split(".") 想按句号切分,会发现得到的是空数组,因为 . 在正则里是“任意字符”的意思。类似的还有 |、*、+、\\ 等元字符。
正确的做法是:
java复制String[] parts = s.split("\\.");
String[] parts2 = s.split("\\|");
String[] parts3 = s.split("\\*");
另外一个容易被忽视的坑:split 默认会去掉尾部的空字符串。比如 "a,b,".split(",") 得到的是 ["a", "b"],而不是 ["a", "b", ""]。竞赛中一旦题目要求精确切分,这就成了 bug 之源。解决办法是用 split(",", -1),这里的 -1 表示保留所有尾部空串。
5.4 比较字符串到底用 == 还是 equals
这个知识点对初学者来说几乎必错。== 比较的是引用地址,equals 比较的是内容。但问题是 JVM 对字符串常量做了一个缓存机制,所以有时候字面量相同的两个 String 用 == 比较也是 true,这就让很多人产生了“== 也能用”的错觉。
java复制String a = "hello";
String b = "hello";
System.out.println(a == b); // true,因为都是常量池里的同一个对象
String c = new String("hello");
System.out.println(a == c); // false,c 是 new 出来的新对象
System.out.println(a.equals(c)); // true
竞赛里判断字符串是否相等,一律用 equals(),不解释。如果有大量字符串要当 key 用,那就用 intern() 配合 == 可以提速,但这个操作本身也有成本,不了解就别乱用。
5.5 编译环境相关:源发行版 17 需要目标发行版 17 这类错误
热搜词里有一条“源发行版 17 需要目标发行版 17”,这其实是竞赛党用 IDE 的时候最讨厌也最常见的报错。它的本质是编译器版本和运行环境版本不匹配,也就是你编译源代码时用的是 Java 17 的语法和类库,但项目配置的目标版本是更低的 JDK,比如 8,编译就过不去。
排查思路很简单:看 IDE 里 Project Structure 的 SDK 和 language level 是否一致,看 Maven/Gradle 里 maven.compiler.source 和 target 是否设成了同一个版本。竞赛准备环境时,建议直接用 JDK 17 并保持语言级别和运行环境一致,省去这些麻烦。
还要提一句 lombok 报错,you aren't using a compiler supported by lombok 是版本兼容问题。竞赛刷题的代码里建议别用 lombok,自己写 getter/setter 又不费多少时间,还能避免裁判机编译不通过。
6. 一些额外的字符串技巧与竞赛经验
6.1 ASCII 码与字符运算的妙用
很多时候字符串题不需要真正操作字符串,而是操作字符对应的数字。比如判断一个字符是不是数字:c >= '0' && c <= '9',判断是不是大写字母:c >= 'A' && c <= 'Z',大小写转换:c ^= 32。
c ^= 32 这个技巧很冷门但很好用。大写字母和小写字母的 ASCII 码正好相差 32,也就是第 6 位比特不同。所以你要把大写转小写或者小写转大写,直接异或 32 就行,比加 32 减 32 快,而且不需要判断当前是大写还是小写。
6.2 从 String 到 char[] 再到 String 的循环
竞赛中要修改字符串内容,我的习惯是“三步走”:先 toCharArray() 变成数组,在数组上完成所有修改,最后 new String(arr) 转回来。这样既绕开了 String 不可变性的限制,又比 StringBuilder 的 append/delete 更灵活,尤其在处理交换、反转、条件替换这类操作时,数组的下标访问是最自然的。
反而 StringBuilder 的操作长链写久了容易漏掉中间某个 append,可读性也不如数组直观。但涉及“动态拼接”时 StringBuilder 又会反过来胜出。我给一个自己的选择标准:改内容用 char[],拼内容用 StringBuilder,查内容用 String 原生的 API。
6.3 常见问题速查表
我把上面所有提到的坑整理成一个速查表,刷题前扫一遍,能挡掉不少 WA 和 TLE。
| 问题场景 | 错误写法 | 正确写法 |
|---|---|---|
| 循环拼接字符串 | s += c |
StringBuilder.append(c) |
| 按句号切分 | s.split(".") |
s.split("\\.") |
| 比较字符串内容 | s1 == s2 |
s1.equals(s2) |
| 获取子串后忽略返回 | s.substring(1, 3) 没有赋值 |
String sub = s.substring(1, 3) |
| 修改字符串某个字符 | s.charAt(0) = 'a' |
先 toCharArray 再赋值,或 StringBuilder.setCharAt |
| 频繁截取子串做比较 | s.substring(0, k).equals(p) |
用 indexOf、startsWith 或哈希 |
| 大数相加 | 用 long 硬存 | 用字符串模拟竖式或 BigInteger |
6.4 实战中省时间的输入输出技巧
最后分享一个很多人会忽略的点:在竞赛里,字符串的读入和输出也会成为性能瓶颈。Scanner.nextLine() 在数据量小的题目里没问题,但一旦有 10^5 行字符串输入,Scanner 就会拖后腿。建议用 BufferedReader 读取,配合 StringTokenizer 或直接 split 处理。
java复制BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = br.readLine()) != null) {
// 处理每行字符串
String[] parts = line.split("\\s+");
}
输出方面,不要反复调用 System.out.println(),而是用 StringBuilder 收集所有输出,最后一次性打印:
java复制StringBuilder out = new StringBuilder();
out.append(ans).append('\n');
// 循环结束后
System.out.print(out);
这个输出技巧在数据量大的时候真的能帮你从 TLE 边缘拉回来,属于比赛老手的保留手段。
我在实际打比赛的过程中发现,字符串这块题目虽然花样多,但套路是有限的。把上面那几套模板反复用熟,再配合对数据结构的敏感度,遇到大部分字符串题都能快速找到突破口。尤其这次总结的哈希模板、KMP 模板、滑动窗口模板,我基本上每场积分赛都用得上,属于性价比极高的投入。你刷题的时候如果遇到新的字符串题型,也建议把代码模板积累到自己的笔记里,时间久了就是一笔巨大的财富。
