一、字符串处理核心知识点
1. Java 字符串特性
(1)不可变性(Immutable)
- 本质 :
String
类基于final char[] value
实现,一经创建不可修改,任何修改操作都会生成新对象。 - 应用场景 :
-
频繁修改字符串时,使用
StringBuilder
(非线程安全)或StringBuffer
(线程安全),其内部维护可变字符数组,避免频繁创建对象。// 反例:循环中用 + 拼接字符串(时间复杂度 O(n²))
String s = "";
for (int i = 0; i < n; i++) {
s += i; // 每次拼接生成新字符串
}// 正例:使用 StringBuilder(时间复杂度 O(n))
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(i);
}
-
(2)常用 API 详解
方法 | 描述 | 时间复杂度 |
---|---|---|
charAt(int index) |
返回指定索引处的字符,索引范围 [0, length()-1] ,越界抛出异常 |
O(1) |
substring(int begin) substring(int begin, int end) |
截取子串,左闭右开区间 [begin, end) ,不包含 end 索引字符 |
O(k) |
toCharArray() |
将字符串转换为字符数组 | O(n) |
indexOf(String str) lastIndexOf(String str) |
查找子串首次 / 末次出现的位置,未找到返回 -1 |
O(n×m) |
matches(String regex) |
验证字符串是否匹配正则表达式 | O(n) |
split(String regex) |
根据正则表达式分割字符串,返回字符串数组 | O(n) |
2. 高频解题技巧
技巧 | 核心思想 | 典型例题(难度) | 优化点 |
---|---|---|---|
双指针 | 快慢指针或左右指针,用于遍历、交换、验证回文等场景 | 125. 验证回文串(简单) 344. 反转字符串(简单) | 原地操作,空间复杂度 O (1) |
滑动窗口 | 通过维护窗口 [left, right] 动态调整区间,解决子串 / 子数组问题 | 3. 无重复字符的最长子串(中等) 76. 最小覆盖子串(困难) | 用哈希表 / 数组记录字符频率,优化窗口收缩逻辑 |
哈希映射(HashMap) | 统计字符频率、快速查找字符位置,适用于异位词、字符匹配问题 | 387. 字符串中的第一个唯一字符(简单) 49. 字母异位词分组(中等) | 用 int[128] 替代 HashMap ,提升访问速度(尤其针对 ASCII 字符) |
动态规划(DP) | 定义状态转移方程,解决最长公共子串、最长回文子串等问题 | 5. 最长回文子串(中等) 1143. 最长公共子序列(中等) | 状态压缩(如二维数组压缩为一维),降低空间复杂度 |
栈结构 | 处理括号匹配、表达式求值、路径简化等具有后进先出特性的问题 | 20. 有效的括号(简单) 71. 简化路径(中等) | 用 Deque 替代 Stack ,优先使用 ArrayDeque (性能更优) |
二、经典题型解析(附变形题与优化思路)
1. 反转字符串(No. 344)
问题描述 :原地反转字符数组,要求空间复杂度 O (1)。
双指针解法:
public void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while (left < right) {
// 交换左右指针字符
char temp = s[left];
s[left++] = s[right];
s[right--] = temp;
}
}
变形题 :反转字符串中的单词(No. 151)
解法思路:
-
去除首尾空格,按多个空格分割单词;
-
反转单词列表,用空格拼接结果。
public String reverseWords(String s) {
// 1. 去除首尾空格并按空格分割(+ 表示匹配一个或多个空格)
String[] words = s.trim().split(" +");
// 2. 反转单词列表
Collections.reverse(Arrays.asList(words));
// 3. 用空格拼接结果
return String.join(" ", words);
}
优化点 :避免使用 split
产生的空字符串,改用双指针手动分割单词。
2. 最长无重复子串(No. 3)
滑动窗口 + 哈希表解法:
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>(); // 记录字符最后出现的位置
int maxLen = 0, left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
// 窗口左边界至少移动到重复字符的下一个位置
left = Math.max(left, map.get(c) + 1);
}
map.put(c, right); // 更新字符位置
maxLen = Math.max(maxLen, right - left + 1); // 计算当前窗口长度
}
return maxLen;
}
进阶优化 :使用 int[128]
数组替代 HashMap
(适用于 ASCII 字符集)
public int lengthOfLongestSubstring(String s) {
int[] freq = new int[128]; // 初始化全为 0
int maxLen = 0, left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
freq[c]++;
// 窗口内出现重复字符时,收缩左边界
while (freq[c] > 1) {
freq[s.charAt(left++)]--;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
3. 有效括号(No. 20)
栈结构解法:
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>(); // 优先使用双端队列
Map<Character, Character> map = Map.of(')', '(', ']', '[', '}', '{'); // Java 9+ 语法
for (char c : s.toCharArray()) {
if (!map.containsKey(c)) { // 左括号入栈
stack.push(c);
} else { // 右括号匹配
if (stack.isEmpty() || stack.pop() != map.get(c)) {
return false;
}
}
}
return stack.isEmpty(); // 最终栈为空则匹配成功
}
易错点 :右括号出现时,需先判断栈是否为空,避免 NullPointerException
。
三、进阶算法与复杂题型
1. KMP 算法(No. 28 实现 strStr ())
核心思想 :利用前缀函数(lps
数组)记录模式串的最长公共前后缀,避免重复匹配。
算法步骤:
-
构建前缀表(
lps
数组) :
lps[i]
表示模式串前i
个字符的最长公共前后缀长度(不包含自身)。 -
模式匹配:通过前缀表跳过已匹配的部分,减少不必要的回溯。
public int strStr(String haystack, String needle) {
int n = haystack.length(), m = needle.length();
if (m == 0) return 0; // 空字符串特殊处理// 1. 构建前缀表(lps 数组) int[] lps = new int[m]; for (int i = 1, len = 0; i < m;) { // i 为当前字符索引,len 为最长公共前后缀长度 if (needle.charAt(i) == needle.charAt(len)) { lps[i++] = ++len; // 匹配成功,长度+1 } else if (len > 0) { len = lps[len - 1]; // 回溯到上一个最长公共前后缀 } else { lps[i++] = 0; // 无公共前后缀 } } // 2. 模式匹配 for (int i = 0, j = 0; i < n;) { // i 为主串索引,j 为模式串索引 if (haystack.charAt(i) == needle.charAt(j)) { i++; j++; if (j == m) return i - m; // 匹配成功,返回起始位置 } else if (j > 0) { j = lps[j - 1]; // 模式串回溯到 lps[j-1] 位置 } else { i++; // 主串后移一位 } } return -1; // 未找到匹配
}
时间复杂度:构建前缀表 O (m),匹配过程 O (n),总体 O (n+m)。
2. 字符串排列(No. 567 验证子串是否为排列)
滑动窗口 + 频率数组解法:
public boolean checkInclusion(String s1, String s2) {
int[] count = new int[26]; // 记录 s1 字符频率
for (char c : s1.toCharArray()) {
count[c - 'a']++;
}
int left = 0, right = 0, match = 0; // match 记录匹配的字符数
while (right < s2.length()) {
int rChar = s2.charAt(right) - 'a';
if (count[rChar] > 0) { // 字符在 s1 中存在
count[rChar]--;
match++;
right++;
} else { // 字符不在 s1 中,重置窗口
count[s2.charAt(left) - 'a']++; // 左边界字符放回频率数组
if (match > 0 && s2.charAt(left) - 'a' == rChar) { // 若移除的是不匹配字符,重置 match
match = 0;
}
left++;
right = left; // 右边界回到左边界,重新开始匹配
}
if (match == s1.length()) { // 窗口内所有字符匹配 s1
return true;
}
}
return false;
}
优化点 :通过 match
变量记录匹配进度,避免每次比较整个窗口的频率数组。
四、常见易错点与避坑指南
易错点 | 示例代码(反例) | 修正方法 |
---|---|---|
索引越界 | char c = s.charAt(s.length()); |
先判断 s.length() > 0 ,或使用 try-catch 捕获异常 |
字符串比较误区 | if (s1 == s2) (比较引用而非内容) |
使用 s1.equals(s2) ,或对可能为 null 的对象用 Objects.equals(s1, s2) |
不可变陷阱 | for (int i=0; i<n; i++) s += i; (低效拼接) |
改用 StringBuilder.append() |
字符与数字转换 | int num = 'a' - 'A'; (正确,得 32) int digit = s.charAt(i) - '0' (需确保字符是数字) |
转换前用 Character.isDigit(c) 验证,避免非法字符导致错误 |
边界条件遗漏 | 空字符串 "" 、全空格 " " 、超长整数 "2147483648" (超出 Integer 范围) |
测试用例中必须包含空输入、极端值,处理大数时用 long 过渡并判断溢出 |
正则表达式转义 | s.split(".") (无法分割,. 是正则通配符) |
转义为 s.split("\\.") ,或使用 Pattern.quote(".") |
五、系统化刷题策略
1. 分阶段训练路线
阶段 | 目标 | 推荐题型(题号 / 难度) | 学习要点 |
---|---|---|---|
基础 | 掌握字符串特性与基础 API | 344. 反转字符串(简单) 20. 有效括号(简单) | 双指针、栈的基本使用 |
进阶 | 熟练运用滑动窗口、哈希表 | 3. 无重复字符的最长子串(中等) 49. 字母异位词分组(中等) | 窗口收缩逻辑、字符频率统计 |
高阶 | 动态规划、KMP 等复杂算法 | 5. 最长回文子串(中等) 28. 实现 strStr ()(中等) | 状态转移方程设计、前缀函数理解 |
挑战 | 综合应用与优化 | 76. 最小覆盖子串(困难) 567. 字符串排列(中等) | 多技巧结合、性能优化(如用数组替代哈希表) |
2. 测试用例设计模板
// 以反转字符串为例
public class TestReverseString {
@Test
public void testCases() {
// 1. 空输入
char[] s1 = {};
reverseString(s1);
assertArrayEquals(new char[]{}, s1);
// 2. 单字符
char[] s2 = {'a'};
reverseString(s2);
assertArrayEquals(new char[]{'a'}, s2);
// 3. 偶数长度
char[] s3 = {'h', 'e', 'l', 'l', 'o'};
reverseString(s3);
assertArrayEquals(new char[]{'o', 'l', 'l', 'e', 'h'}, s3);
// 4. 奇数长度
char[] s4 = {'A', ' ', 'm', 'a', 'n'};
reverseString(s4);
assertArrayEquals(new char[]{'n', 'a', 'm', ' ', 'A'}, s4);
}
}
3. 代码模板总结
(1)双指针模板(反转 / 回文)
public void twoPointersTemplate(char[] s) {
int left = 0, right = s.length - 1;
while (left < right) {
// 交换/比较操作
char temp = s[left];
s[left++] = s[right];
s[right--] = temp;
}
}
(2)滑动窗口模板(子串问题)
public int slidingWindowTemplate(String s) {
int[] freq = new int[128];
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
freq[c]++;
// 窗口收缩条件(根据题意调整)
while (freq[c] > 1) {
freq[s.charAt(left++)]--;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
通过系统掌握字符串处理的核心技巧,配合合理的Java API使用,能够显著提升解决LeetCode字符串类题目的效率。建议从简单题开始建立信心,逐步挑战中等难度综合题,最后攻克KMP等高级算法难题。