1. 概述
在日常Java开发中,字符串处理是基础且频繁的操作。有时我们需要将字符串旋转n个字符——这种操作本质上是将字符进行循环移位,形成动态的视觉效果。
本文将探讨解决字符串旋转问题的多种方案,并分析其实现原理。
2. 问题解析
字符串旋转n字符指的是将字符串中的字符循环移动n位。通过示例可以快速理解这个概念。
2.1. 示例演示
假设有字符串对象:
String STRING = "abcdefg";
正向旋转结果如下:
- 正向旋转 -
输入字符串 : abcdefg
旋转 (n = 1) -> gabcdef
旋转 (n = 2) -> fgabcde
旋转 (n = 3) -> efgabcd
旋转 (n = 4) -> defgabc
旋转 (n = 5) -> cdefgab
旋转 (n = 6) -> bcdefga
旋转 (n = 7) -> abcdefg
旋转 (n = 8) -> gabcdef
...
反向旋转结果如下:
- 反向旋转 -
输入字符串 : abcdefg
旋转 (n = 1) -> bcdefga
旋转 (n = 2) -> cdefgab
旋转 (n = 3) -> defgabc
旋转 (n = 4) -> efgabcd
旋转 (n = 5) -> fgabcde
旋转 (n = 6) -> gabcdef
旋转 (n = 7) -> abcdefg
...
2.2. 问题分析
在编码前,先分析问题特性:
✅ 旋转0字符:无论方向如何,结果等于原字符串
✅ 旋转长度倍数:当n等于字符串长度时,结果等于原字符串
✅ 周期性规律:旋转n字符等同于旋转n % STRING.length字符
✅ 方向转换:反向旋转n字符 = 正向旋转(STRING.length - n)字符
关键结论:
- 有效旋转位数:
n' = n % STRING.length
- 当n=0或n=K×STRING.length时:
result = STRING
- 反向旋转可转换为正向旋转:
backward(n) = forward(len - n)
2.3. 测试数据准备
为验证方案,准备测试用例:
// 正向旋转
String EXPECT_1X = "gabcdef";
String EXPECT_2X = "fgabcde";
String EXPECT_3X = "efgabcd";
String EXPECT_6X = "bcdefga";
String EXPECT_7X = "abcdefg"; // len = 7
String EXPECT_24X = "efgabcd"; //24 = 3 x 7(len) + 3
// 反向旋转
String B_EXPECT_1X = "bcdefga";
String B_EXPECT_2X = "cdefgab";
String B_EXPECT_3X = "defgabc";
String B_EXPECT_6X = "gabcdef";
String B_EXPECT_7X = "abcdefg";
String B_EXPECT_24X = "defgabc";
3. 分割重组法
核心思路:将字符串分割为两个子串,交换位置后重组。以正向旋转2位为例:
索引 0 1 2 3 4 5 6
STRING a b c d e f g
子串1 [a b c d e] -->
子串2 <-- [f g]
结果 [f g] [a b c d e]
关键点:
- 子串1范围:
[0, len-n)
→[0,5)
- 子串2范围:
[len-n, len)
→[5,7)
⚠️ 注意:Java的substring()
方法采用左闭右开区间,与我们的计算方式完美契合。
实现代码:
String rotateString1(String s, int c, boolean forward) {
if (c < 0) {
throw new IllegalArgumentException("旋转位数不能为负数!");
}
int len = s.length();
int n = c % len;
if (n == 0) {
return s;
}
n = forward ? n : len - n;
return s.substring(len - n, len) + s.substring(0, len - n);
}
测试验证:
// 正向旋转
assertEquals(EXPECT_1X, rotateString1(STRING, 1, true));
assertEquals(EXPECT_2X, rotateString1(STRING, 2, true));
assertEquals(EXPECT_3X, rotateString1(STRING, 3, true));
assertEquals(EXPECT_6X, rotateString1(STRING, 6, true));
assertEquals(EXPECT_7X, rotateString1(STRING, 7, true));
assertEquals(EXPECT_24X, rotateString1(STRING, 24, true));
// 反向旋转
assertEquals(B_EXPECT_1X, rotateString1(STRING, 1, false));
assertEquals(B_EXPECT_2X, rotateString1(STRING, 2, false));
assertEquals(B_EXPECT_3X, rotateString1(STRING, 3, false));
assertEquals(B_EXPECT_6X, rotateString1(STRING, 6, false));
assertEquals(B_EXPECT_7X, rotateString1(STRING, 7, false));
assertEquals(B_EXPECT_24X, rotateString1(STRING, 24, false));
4. 自连接提取法
核心思想:将字符串与自身拼接(SS = STRING + STRING),旋转结果必为SS的子串。以正向旋转2位为例:
索引 0 1 2 3 4 5 6 | 7 8 9 10 11 12 13
SS a b c d e f g | a b c d e f g
|
结果 a b c d e [f g a b c d e] f g
关键索引计算:
- 起始位置:
len - n
→7 - 2 = 5
- 结束位置:
2*len - n
→14 - 2 = 12
实现代码:
String rotateString2(String s, int c, boolean forward) {
if (c < 0) {
throw new IllegalArgumentException("旋转位数不能为负数!");
}
int len = s.length();
int n = c % len;
if (n == 0) {
return s;
}
String ss = s + s;
n = forward ? n : len - n;
return ss.substring(len - n, 2 * len - n);
}
测试验证:
// 正向旋转
assertEquals(EXPECT_1X, rotateString2(STRING, 1, true));
assertEquals(EXPECT_2X, rotateString2(STRING, 2, true));
assertEquals(EXPECT_3X, rotateString2(STRING, 3, true));
assertEquals(EXPECT_6X, rotateString2(STRING, 6, true));
assertEquals(EXPECT_7X, rotateString2(STRING, 7, true));
assertEquals(EXPECT_24X, rotateString2(STRING, 24, true));
// 反向旋转
assertEquals(B_EXPECT_1X, rotateString2(STRING, 1, false));
assertEquals(B_EXPECT_2X, rotateString2(STRING, 2, false));
assertEquals(B_EXPECT_3X, rotateString2(STRING, 3, false));
assertEquals(B_EXPECT_6X, rotateString2(STRING, 6, false));
assertEquals(B_EXPECT_7X, rotateString2(STRING, 7, false));
assertEquals(B_EXPECT_24X, rotateString2(STRING, 24, false));
💡 延伸应用:利用此特性可判断字符串是否由另一字符串旋转得到:
boolean rotatedFrom(String rotated, String rotateFrom) {
return rotateFrom.length() == rotated.length()
&& (rotateFrom + rotateFrom).contains(rotated);
}
测试验证:
assertTrue(rotatedFrom(EXPECT_7X, STRING));
assertTrue(rotatedFrom(B_EXPECT_3X, STRING));
assertFalse(rotatedFrom("abcefgd", STRING));
5. 总结
本文深入分析了Java字符串旋转问题,并提供了两种高效解决方案:
- 分割重组法:通过子串分割与重组实现旋转
- 自连接提取法:利用字符串拼接特性提取目标子串
两种方法均通过测试验证,开发者可根据实际场景选择。完整代码示例可在GitHub仓库获取。