仅仅反转字母
问题描述
给你一个字符串 s
,根据以下规则反转字符串:
- 所有非字母字符保持原来的位置
- 所有字母字符反转它们的位置
返回反转后的字符串。
示例:
输入: s = "ab-cd"
输出: "dc-ba"
输入: s = "a-bC-dEf-ghIj"
输出: "j-Ih-gfE-dCba"
输入: s = "Test1ng-Leet=code-Q!"
输出: "Qedo1ct-eeLg=ntse-T!"
算法思路
核心思路:
- 双指针法:使用左右两个指针,分别从字符串两端向中间移动
- 跳过非字母字符:当指针遇到非字母字符时,继续移动直到找到字母字符
- 交换字母字符:当左右指针都指向字母字符时,交换它们的位置
关键点:
- 需要正确判断字符是否为字母(包括大小写字母)
- 双指针相遇时停止,避免重复交换
- 保持非字母字符的原始位置不变
代码实现
方法一:双指针 + 字符数组
class Solution {
/**
* 仅仅反转字符串中的字母字符,非字母字符保持原位置
* 使用双指针法,时间复杂度O(n),空间复杂度O(n)
*
* @param s 输入字符串
* @return 反转字母后的字符串
*/
public String reverseOnlyLetters(String s) {
// 边界情况处理
if (s == null || s.length() <= 1) {
return s;
}
// 转换为字符数组便于修改
char[] chars = s.toCharArray();
int left = 0; // 左指针
int right = s.length() - 1; // 右指针
// 双指针向中间移动
while (left < right) {
// 左指针跳过非字母字符
while (left < right && !Character.isLetter(chars[left])) {
left++;
}
// 右指针跳过非字母字符
while (left < right && !Character.isLetter(chars[right])) {
right--;
}
// 交换左右指针指向的字母字符
if (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
}
return new String(chars);
}
}
方法二:手动判断字母
class Solution {
/**
* 手动实现字母判断,不依赖Character.isLetter()
*
* @param s 输入字符串
* @return 反转字母后的字符串
*/
public String reverseOnlyLetters(String s) {
if (s == null || s.length() <= 1) {
return s;
}
char[] chars = s.toCharArray();
int left = 0;
int right = s.length() - 1;
while (left < right) {
// 左指针找字母
while (left < right && !isLetter(chars[left])) {
left++;
}
// 右指针找字母
while (left < right && !isLetter(chars[right])) {
right--;
}
// 交换字母
if (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
}
return new String(chars);
}
/**
* 手动判断字符是否为字母
*
* @param c 待判断字符
* @return 是否为字母
*/
private boolean isLetter(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
}
方法三:使用栈
import java.util.*;
class Solution {
/**
* 使用栈存储所有字母,然后重新构建字符串
* 空间复杂度较高,但思路直观
*
* @param s 输入字符串
* @return 反转字母后的字符串
*/
public String reverseOnlyLetters(String s) {
if (s == null || s.length() <= 1) {
return s;
}
// 将所有字母压入栈中(自然反转顺序)
Stack<Character> letterStack = new Stack<>();
for (char c : s.toCharArray()) {
if (Character.isLetter(c)) {
letterStack.push(c);
}
}
// 重新构建字符串
StringBuilder result = new StringBuilder();
for (char c : s.toCharArray()) {
if (Character.isLetter(c)) {
// 字母位置用栈顶元素(反转后的字母)
result.append(letterStack.pop());
} else {
// 非字母字符保持原样
result.append(c);
}
}
return result.toString();
}
}
算法分析
- 时间复杂度:O(n)
- 双指针法:每个字符最多被访问两次
- 栈方法:需要两次遍历字符串
- 空间复杂度:
- 双指针法:O(n) - 字符数组存储
- 栈方法:O(n) - 栈存储所有字母 + StringBuilder
- 方法对比:
- 双指针法:空间效率高,一次遍历完成,推荐使用
- 栈方法:思路直观,但空间开销大
- 手动判断:避免依赖内置方法
算法过程
输入:s = "a-bC-dEf-ghIj"
双指针过程:
初始状态:['a', '-', 'b', 'C', '-', 'd', 'E', 'f', '-', 'g', 'h', 'I', 'j']
- left=0 (‘a’), right=12 (‘j’) → 交换 →
['j', '-', 'b', 'C', '-', 'd', 'E', 'f', '-', 'g', 'h', 'I', 'a']
- left=1 (‘-’) 跳过 → left=2 (‘b’)
- right=11 (‘I’) → 交换 →
['j', '-', 'I', 'C', '-', 'd', 'E', 'f', '-', 'g', 'h', 'b', 'a']
- left=3 (‘C’), right=10 (‘h’) → 交换 →
['j', '-', 'I', 'h', '-', 'd', 'E', 'f', '-', 'g', 'C', 'b', 'a']
- left=4 (‘-’) 跳过 → left=5 (‘d’)
- right=9 (‘g’) → 交换 →
['j', '-', 'I', 'h', '-', 'g', 'E', 'f', '-', 'd', 'C', 'b', 'a']
- left=6 (‘E’), right=8 (‘-’) 跳过 → right=7 (‘f’) → 交换 →
['j', '-', 'I', 'h', '-', 'g', 'f', 'E', '-', 'd', 'C', 'b', 'a']
- left=7, right=6 → left >= right,结束
结果:"j-Ih-gfE-dCba"
栈方法过程:
- 提取字母:
['a', 'b', 'C', 'd', 'E', 'f', 'g', 'h', 'I', 'j']
- 压入栈后:栈顶到栈底为
['j', 'I', 'h', 'g', 'f', 'E', 'd', 'C', 'b', 'a']
- 重建字符串:
- ‘a’ → ‘j’
- ‘-’ → ‘-’
- ‘b’ → ‘I’
- ‘C’ → ‘h’
- …
- 结果:
"j-Ih-gfE-dCba"
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
System.out.println("Test 1: " + solution.reverseOnlyLetters("ab-cd")); // "dc-ba"
System.out.println("Test 2: " + solution.reverseOnlyLetters("a-bC-dEf-ghIj")); // "j-Ih-gfE-dCba"
System.out.println("Test 3: " + solution.reverseOnlyLetters("Test1ng-Leet=code-Q!")); // "Qedo1ct-eeLg=ntse-T!"
// 测试用例2:无字母
System.out.println("Test 4: " + solution.reverseOnlyLetters("123!@#")); // "123!@#"
// 测试用例3:全字母
System.out.println("Test 5: " + solution.reverseOnlyLetters("abcdef")); // "fedcba"
System.out.println("Test 6: " + solution.reverseOnlyLetters("ABCDEF")); // "FEDCBA"
// 测试用例4:单字符
System.out.println("Test 7: " + solution.reverseOnlyLetters("a")); // "a"
System.out.println("Test 8: " + solution.reverseOnlyLetters("1")); // "1"
System.out.println("Test 9: " + solution.reverseOnlyLetters("-")); // "-"
// 测试用例5:空字符串
System.out.println("Test 10: " + solution.reverseOnlyLetters("")); // ""
// 测试用例6:只有非字母
System.out.println("Test 11: " + solution.reverseOnlyLetters("!@#$%^&*()")); // "!@#$%^&*()"
// 测试用例7:字母和数字混合
System.out.println("Test 12: " + solution.reverseOnlyLetters("a1b2c3")); // "c1b2a3"
// 测试用例8:大小写字母混合
System.out.println("Test 13: " + solution.reverseOnlyLetters("AbCdEf")); // "fEdCbA"
// 测试用例9:边界情况
System.out.println("Test 14: " + solution.reverseOnlyLetters("a-")); // "a-"
System.out.println("Test 15: " + solution.reverseOnlyLetters("-a")); // "-a"
// 测试用例10:长字符串
StringBuilder longStr = new StringBuilder();
for (int i = 0; i < 100; i++) {
if (i % 3 == 0) {
longStr.append((char)('a' + i % 26));
} else {
longStr.append('-');
}
}
System.out.println("Test 16: Length = " + solution.reverseOnlyLetters(longStr.toString()).length()); // 验证长度不变
// 测试用例11:null值
System.out.println("Test 17: " + solution.reverseOnlyLetters(null)); // null
}
关键点
-
双指针的核心思想:
- 左右指针分别寻找字母字符
- 跳过非字母字符,只交换字母字符
- 保证非字母字符位置不变
-
字母判断方法:
Character.isLetter()
:Java内置方法,支持Unicode- 手动判断:
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
,仅支持ASCII
-
边界处理:
- 空字符串和null值
- 单字符字符串
- 无字母或全字母的特殊情况
-
空间效率:
- 双指针法只需要字符数组的空间
- 栈方法需要额外的栈空间存储所有字母
常见问题
-
为什么不用String直接操作?
- Java中String是不可变的,无法直接修改字符
- 必须转换为字符数组或使用StringBuilder
-
Character.isLetter()和手动判断有什么区别?
Character.isLetter()
支持所有Unicode字母- 手动判断只支持ASCII字母(A-Z, a-z)
-
双指针法的时间复杂度真的是O(n)吗?
- 虽然有嵌套循环,但每个字符最多被访问两次
- 左指针和右指针总共移动n次,所以是O(n)