字符串处理是编程中最基础也是最重要的技能之一,在Java开发中尤其如此。无论是日常业务开发还是算法面试,字符串相关的题目都占据了相当大的比重。本文将从底层原理到高级算法,系统性地讲解Java字符串处理的核心技巧和解题思路。
提示:在ASCII码中,大小写英文字母和数字的十进制数值范围如下:
- 大写字母(A-Z):65到90
- 小写字母(a-z):97到122
- 数字(0-9):48到57
字符串在Java中是不可变对象,这意味着每次修改字符串都会创建一个新的对象。理解这一点对于编写高效的字符串处理代码至关重要。
java复制String s = "hello";
// 获取字符串长度
int len = s.length(); // 5
// 获取指定位置字符
char c = s.charAt(0); // 'h'
// 截取子串(左闭右开)
String sub = s.substring(1, 4); // "ell"
// 查找字符首次出现位置
int index = s.indexOf('l'); // 2
// 判断是否包含子串
boolean contains = s.contains("ell"); // true
// 字符串内容比较
boolean equals = s.equals("hello"); // true (不要用==比较字符串内容)
字符串和字符数组之间的转换是字符串处理中的常见操作:
java复制// String → char[]
char[] chars = s.toCharArray();
// char[] → String
String s2 = new String(chars);
// String分割为String[]
String[] arr = "a,b,c".split(","); // ["a", "b", "c"]
// String[]拼接为String
String s3 = String.join(",", arr); // "a,b,c"
在循环中拼接字符串时,务必使用StringBuilder而不是直接使用+操作符:
java复制// 错误示例:每次循环都会创建新对象
String s = "";
for (int i = 0; i < 10000; i++) {
s += "a"; // O(n²)时间复杂度
}
// 正确示例:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a"); // O(n)时间复杂度
}
String result = sb.toString();
Java提供了Character类来处理字符的各种判断和转换:
java复制char c = 'a';
// 判断字符类型
boolean isDigit = Character.isDigit(c); // 是否数字
boolean isLetter = Character.isLetter(c); // 是否字母
boolean isLetterOrDigit = Character.isLetterOrDigit(c); // 是否字母或数字
boolean isLowerCase = Character.isLowerCase(c); // 是否小写
// 字符转换
char lower = Character.toLowerCase('A'); // 'a'
char upper = Character.toUpperCase('a'); // 'A'
// 字符转数字
int num = '5' - '0'; // 5
哈希表是解决字符串统计类问题的利器,特别是需要统计字符出现频率的场景。
java复制// 方法1:使用HashMap统计(通用)
public Map<Character, Integer> countChars(String s) {
Map<Character, Integer> map = new HashMap<>();
for (char c : s.toCharArray()) {
map.put(c, map.getOrDefault(c, 0) + 1);
}
return map;
}
// 方法2:使用数组统计(仅限ASCII字符,更快)
public int[] countCharsArray(String s) {
int[] count = new int[128]; // 覆盖所有ASCII字符
for (char c : s.toCharArray()) {
count[c]++;
}
return count;
}
java复制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']--;
if (count[c - 'a'] < 0) {
return false;
}
}
return true;
}
java复制public int firstUniqChar(String s) {
int[] count = new int[26];
// 第一次遍历统计频次
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
// 第二次遍历查找第一个唯一字符
for (int i = 0; i < s.length(); i++) {
if (count[s.charAt(i) - 'a'] == 1) {
return i;
}
}
return -1;
}
双指针技术常用于处理字符串反转、回文等问题。
java复制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;
left++;
right--;
}
}
java复制public boolean isPalindrome(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
// 跳过非字母数字字符
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
// 比较字符(忽略大小写)
if (Character.toLowerCase(s.charAt(left)) !=
Character.toLowerCase(s.charAt(right))) {
return false;
}
left++;
right--;
}
return true;
}
滑动窗口是解决子串问题的强大技术,可以将时间复杂度从O(n²)优化到O(n)。
java复制public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) return 0;
int left = 0, maxLen = 0;
Map<Character, Integer> map = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果字符已存在且在窗口内,移动左指针
if (map.containsKey(c) && map.get(c) >= left) {
left = map.get(c) + 1;
}
map.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
java复制public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
int[] pCount = new int[26];
int[] sCount = new int[26];
// 统计p的字符频次
for (char c : p.toCharArray()) {
pCount[c - 'a']++;
}
// 初始化第一个窗口
for (int i = 0; i < p.length(); i++) {
sCount[s.charAt(i) - 'a']++;
}
if (Arrays.equals(pCount, sCount)) {
result.add(0);
}
// 滑动窗口
for (int i = p.length(); i < s.length(); i++) {
// 加入新字符
sCount[s.charAt(i) - 'a']++;
// 移除旧字符
sCount[s.charAt(i - p.length()) - 'a']--;
if (Arrays.equals(pCount, sCount)) {
result.add(i - p.length() + 1);
}
}
return result;
}
动态规划常用于解决最长公共子串、最长回文子串等问题。
java复制public String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s;
int start = 0, maxLen = 1;
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) {
maxLen = len;
start = i - (len - 1) / 2;
}
}
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;
}
栈结构非常适合处理括号匹配、路径简化等具有"最近匹配"特性的问题。
java复制public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return stack.isEmpty();
}
java复制public String simplifyPath(String path) {
Stack<String> stack = new Stack<>();
String[] parts = path.split("/");
for (String part : parts) {
if (part.equals("..")) {
if (!stack.isEmpty()) {
stack.pop();
}
} else if (!part.equals(".") && !part.isEmpty()) {
stack.push(part);
}
}
if (stack.isEmpty()) return "/";
StringBuilder sb = new StringBuilder();
for (String dir : stack) {
sb.append("/").append(dir);
}
return sb.toString();
}
识别问题类型:根据题目特征判断属于哪类问题
从暴力解法开始:先思考最直接的解法,再考虑优化
选择合适的数据结构:根据问题特点选择最高效的数据结构
字符串不可变性:避免在循环中直接拼接字符串,使用StringBuilder
字符串比较:使用equals()而不是==比较字符串内容
边界条件处理:空字符串、单字符字符串等特殊情况
索引越界:注意字符串索引范围是[0, length-1]
字符编码:处理非ASCII字符时考虑Unicode编码
数组替代HashMap:当字符集有限时(如仅小写字母),使用数组比HashMap更高效
提前终止:在满足条件时提前终止循环,减少不必要的计算
空间优化:有些问题可以原地修改输入,减少额外空间使用
预处理:对字符串进行预处理(如排序)可以简化后续处理
java复制// 字符串访问
charAt(index) // 获取指定位置字符
toCharArray() // 转换为字符数组
substring(start, end) // 截取子串(左闭右开)
// 字符串查找
indexOf(str) // 查找子串位置
lastIndexOf(str) // 从后向前查找
contains(str) // 是否包含子串
// 字符串判断
isEmpty() // 是否空字符串
equals(str) // 内容比较
startsWith(prefix) // 是否以指定前缀开始
endsWith(suffix) // 是否以指定后缀结束
// 字符串修改
toLowerCase() // 转换为小写
toUpperCase() // 转换为大写
trim() // 去除首尾空白
replace(old, new) // 替换子串
// 字符串分割与拼接
split(regex) // 按正则分割
String.join(delimiter, elements) // 用分隔符拼接
StringBuilder.append() // 高效拼接
掌握这些字符串处理技巧和算法,你将能够高效解决绝大多数字符串相关问题。记住,理解算法背后的思想比死记硬背代码更重要。在实际应用中,根据具体问题灵活选择和组合这些技术,才能写出既高效又优雅的代码。