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 - n7 - 2 = 5
  • 结束位置:2*len - n14 - 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字符串旋转问题,并提供了两种高效解决方案:

  1. 分割重组法:通过子串分割与重组实现旋转
  2. 自连接提取法:利用字符串拼接特性提取目标子串

两种方法均通过测试验证,开发者可根据实际场景选择。完整代码示例可在GitHub仓库获取。


原始标题:Rotating a Java String By n Characters