Check if Two Strings Are Rotations of Each Other – 检查两个字符串是否互为旋转

最后修改: 2024年 2月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll learn how to check if a string is a rotation of another string.

在本教程中,我们将学习如何检查 string 是否是另一个字符串的旋转。

We’ll briefly discuss what string rotation is. Then, we’ll look at some algorithms to solve the problem with code insight and complexity analysis.

我们将简要讨论什么是字符串旋转。然后,我们将通过代码洞察和复杂性分析来了解一些解决问题的算法。

2. Introduction to String Rotation

2.琴弦旋转简介

Before digging into some solutions, let’s discuss string rotation and what we should test for the algorithm.

在深入探讨一些解决方案之前,让我们先讨论一下字符串旋转以及我们应该对算法进行哪些测试。

2.1. String and String Rotation

2.1.字符串和字符串旋转

A string is a sequence of primitive characters, and in Java, it’s wrapped in a String class. Although two strings might be different objects, we can compare their internal characters and check, for example, whether they’re equal or contain common patterns.

字符串是原始字符的序列,在 Java 中,它被封装在 String 类中。虽然两个字符串可能是不同的对象,但我们可以比较它们的内部字符,例如,检查它们是否相等或包含共同的模式。

A rotation of a string is a string that contains the same characters but in a different order. Specifically, one or more characters are shifted from the original position. For example, the string “cdab” is a rotation of “abcd”. This can be seen in two steps:

字符串旋转是指字符串包含相同的字符,但顺序不同。具体来说,一个或多个字符从原来的位置移动。例如,字符串 “cdab”“abcd” 的旋转。这可以分两步来看:

  • abcd -> dabc shifts the last d to the first position
  • dabc -> cdab shifts the last c to the first position

This is done by shifting from the right, but it could be done also from the left.

这是通过右移实现的,但也可以从左移。

Notably, we can say that if two strings aren’t the same length, they can’t be one rotation of the other.

值得注意的是,我们可以说,如果两根弦的长度不一样,它们就不可能是对方的一个旋转。

2.2. Variable Names

2.2 变量名称

To demonstrate, we’ll always refer to the rotation as the potential string candidate to check whether it’s an actual rotation of an origin.

为了演示,我们将始终把 rotation 作为潜在的候选字符串,以检查它是否是 origin 的实际旋转。

2.3. Unit Testing

2.3.单元测试

We have only two cases to test whether a string is a rotation. Notably, a string is a rotation of itself. Therefore, we might also want to test that corner case.

我们只有两种情况可以测试字符串是否是旋转体。值得注意的是,字符串是自身的旋转。因此,我们可能还想测试这种角情况。

3. Rotation Contained in Doubled String

3.包含在双倍字符串中的旋转

We can naively think that if we double our origin string, at some point, it will contain a rotation. We can picture this intuitively:

我们可以天真地认为,如果我们将原点弦加倍,那么在某个时刻,它将包含旋转。我们可以直观地描绘出这一点:

3.1. Algorithm

3.1.算法

The algorithm is straightforward:

算法简单明了:

boolean doubledOriginContainsRotation(String origin, String rotation) {
    if (origin.length() == rotation.length()) {
        return origin.concat(origin)
          .contains(rotation);
    }

    return false;
}

3.2. Code Insight

3.2.代码洞察力

We concatenate the origin with itself, and we check if it contains the potential rotation:

我们将原点与原点本身连接起来,然后检查原点是否包含潜在的旋转:

return origin.concat(origin).contains(rotation);

Let’s look at the algorithm complexity:

让我们来看看算法的复杂性:

  • Time Complexity: O(n*m) where n is the length of the concatenation and m is the length of the rotation
  • Space Complexity: O(n) proportional to the length of the strings

3.3. Unit Tests

3.3.单元测试

Let’s test the rotation contained in the doubled origin string when the rotation is ok. We’ll also test when the origin is the exact string as the rotation:

让我们来测试在旋转确定时,加倍的原点字符串中包含的旋转。我们还将测试原点是否与旋转字符串完全相同:

@Test
void givenDoubledOrigin_whenCheckIfOriginContainsRotation_thenIsRotation() {
    assertTrue(doubledOriginContainsRotation("abcd", "cdab"));
    assertTrue(doubledOriginContainsRotation("abcd", "abcd"));
}

Let’s test when the rotation isn’t contained. We’ll also test when the rotation is a longer string than the origin:

让我们来测试一下旋转是否不被包含。我们还将测试旋转的字符串是否比原点长:

@Test
void givenDoubledOrigin_whenCheckIfOriginContainsRotation_thenNoRotation() {
    assertFalse(doubledOriginContainsRotation("abcd", "bbbb"));
    assertFalse(doubledOriginContainsRotation("abcd", "abcde"));
}

4. Rotation From Common Start With Origin

4.从原点的共同起点旋转

We can use the previous approach and build a more detailed algorithm.

我们可以利用前一种方法,建立一种更详细的算法。

First, let’s collect all the indexes in the rotation of the starting character of the origin. Finally, we loop over the origin and compare the strings at shifted positions. Let’s picture these steps in more detail:

首先,我们收集原点起始字符旋转中的所有索引。最后,我们在原点上循环,比较移动位置上的字符串。让我们来详细描绘一下这些步骤:

4.1. Algorithm

4.1.算法

Once we know the common characters, we can check whether the strings continue to be equal:

知道共同字符后,我们就可以检查字符串是否继续相等:

boolean isRotationUsingCommonStartWithOrigin(String origin, String rotation) {
    if (origin.length() == rotation.length()) {
        List<Integer> indexes = IntStream.range(0, origin.length())
          .filter(i -> rotation.charAt(i) == origin.charAt(0))
          .boxed()
          .collect(Collectors.toList());

        for (int startingAt : indexes) {
            if (isRotation(startingAt, rotation, origin)) {
                return true;
            }
        }
    }

    return false;
}

boolean isRotation(int startingAt, String rotation, String origin) {
    for (int i = 0; i < origin.length(); i++) {
        if (rotation.charAt((startingAt + i) % origin.length()) != origin.charAt(i)) {
            return false;
        }
    }

    return true;
}

4.2. Code Insight

4.2.代码洞察力

There are two main points to focus on. The first is where we collect the indexes:

有两个要点需要重点关注。首先是我们在哪里收集索引:

List<Integer> indexes = IntStream.range(0, origin.length())
  .filter(i -> rotation.charAt(i) == origin.charAt(0))
  .boxed()
  .collect(Collectors.toList());

These are the positions in the rotation where we can find the starting character of the origin.

在旋转过程中,我们可以在这些位置找到原点的起始字符。

Then, we loop over the strings and check at shifted positions:

然后,我们在字符串上循环,检查移位的位置:

for (int i = 0; i < origin.length(); i++) {
    if (rotation.charAt((startingAt + i) % origin.length()) != origin.charAt(i)) {
        return false;
    }
}

Notably, we use the modulo (%) to return from the first index when we exceed the rotation length.

值得注意的是,当超过旋转长度时,我们会使用 modulo (%) 从第一个索引返回。

Let’s look at the algorithm complexity:

让我们来看看算法的复杂性:

  • Time Complexity: O(n*m) where n is the length of the origin and m is the number of indexes found
  • Space Complexity: O(n)

4.3. Unit Tests

4.3.单元测试

Let’s test when the rotation has common starting characters with the origin and the remaining part of the strings are equal:

让我们测试一下,当旋转的起始字符与原点相同时,字符串的其余部分是否相等:

@Test
void givenOriginAndRotationInput_whenCheckingCommonStartWithOrigin_thenIsRotation() {
    assertTrue(isRotationUsingCommonStartWithOrigin("abcd", "cdab"));
    assertTrue(isRotationUsingCommonStartWithOrigin("abcd", "abcd"));
}

Let’s test when the rotation has common starting characters, but it’s either too long, or the remaining part doesn’t match:

让我们测试一下,当轮换有共同的起始字符,但要么太长,要么剩余部分不匹配时的情况:

@Test
void givenOriginAndRotationInput_whenCheckingCommonStartWithOrigin_thenNoRotation() {
    assertFalse(isRotationUsingCommonStartWithOrigin("abcd", "bbbb"));
    assertFalse(isRotationUsingCommonStartWithOrigin("abcd", "abcde"));
}

5. Rotation Comparing Prefix and Suffix

5.前缀和后缀的旋转比较

If we find a common starting character of origin and rotation, we can also say that our strings will be equal before and after that matching point. For example, our origin string “abcd” has the common c at position 2 with “cdab”. However, prefixes and suffixes will need to be equal accordingly for the remaining portion of the strings:

如果我们找到了原点和旋转的共同起始字符,那么我们也可以说,我们的字符串在该匹配点前后是相等的。例如,我们的起始字符串”abcd”“cdab”在第 2 位有共同的 c。但是,在字符串的其余部分,前缀和后缀需要相应地相等:

5.1. Algorithm

5.1.算法

Whenever we find a common character, we can then compare the prefix and suffix for those segments of characters remaining, inverting the origin and the rotation:

只要找到一个共同字符,我们就可以比较剩余字符段的前缀和后缀,将原点和旋转颠倒过来:

boolean isRotationUsingSuffixAndPrefix(String origin, String rotation) {
    if (origin.length() == rotation.length()) {
        return checkPrefixAndSuffix(origin, rotation);
    }

    return false;
}

boolean checkPrefixAndSuffix(String origin, String rotation) {
    if (origin.length() == rotation.length()) {
        for (int i = 0; i < origin.length(); i++) {
            if (origin.charAt(i) == rotation.charAt(0)) {
                if (checkRotationPrefixWithOriginSuffix(origin, rotation, i)) {
                    if (checkOriginPrefixWithRotationSuffix(origin, rotation, i)) {
                        return true;
                    }
                }
            }
        }
    }

    return false;
}

boolean checkRotationPrefixWithOriginSuffix(String origin, String rotation, int i) {
    return origin.substring(i)
      .equals(rotation.substring(0, origin.length() - i));
}

boolean checkOriginPrefixWithRotationSuffix(String origin, String rotation, int i) {
    return origin.substring(0, i)
      .equals(rotation.substring(origin.length() - i));
}

5.2. Code Insight

5.2.代码洞察力

We have two checks to make. First, we compare the rotation prefix with the origin suffix:

我们需要进行两项检查。首先,我们比较旋转前缀和原点后缀:

return origin.substring(i)
  .equals(rotation.substring(0, origin.length() - i));

Then, we compare the rotation suffix with the origin prefix:

然后,我们将旋转后缀与原点前缀进行比较:

return origin.substring(0, i)
  .equals(rotation.substring(origin.length() - i));

Notably, these checks can be done in any order.

值得注意的是,这些检查可以以任何顺序进行。

Let’s look at the algorithm complexity:

让我们来看看算法的复杂性:

  • Time Complexity: O(n*n) comparing two strings of length n
  • Space Complexity: O(n)

5.3. Unit Tests

5.3.单元测试

Let’s test when the rotation has equal suffix and prefix with the origin given a common character:

让我们来测试一下,当旋转的后缀和前缀与原点的后缀和前缀相同时,是否有一个共同的字符:

@Test
void givenOriginAndRotationInput_whenCheckingUsingSuffixAndPrefix_thenIsRotation() {
    assertTrue(isRotationUsingSuffixAndPrefix("abcd", "cdab"));
    assertTrue(isRotationUsingSuffixAndPrefix("abcd", "abcd"));
}

Let’s test when the rotation doesn’t have an equal suffix and prefix with the origin:

让我们来测试一下当旋转的后缀和前缀与原点不相等时的情况:

@Test
void givenOriginAndRotationInput_whenCheckingUsingSuffixAndPrefix_thenNoRotation() {
    assertFalse(isRotationUsingSuffixAndPrefix("abcd", "bbbb"));
    assertFalse(isRotationUsingSuffixAndPrefix("abcd", "abcde"));
}

6. Rotation Comparing Queue of Characters

6.旋转比较字符队列

Another view of the problem is imagining the two strings as queues. Then, we shift the top character of the rotation into the tail and compare the new queue with the origin. Let’s see a simple picture of the queues:

对这个问题的另一种看法是将两个字符串想象成队列。然后,我们将旋转的顶部字符移到尾部,并将新队列与原点进行比较。让我们来看看队列的简单图片:

6.1. Algorithm

6.1.算法

We create the two queues. Then, we shift from the top character to the bottom of the rotation while checking if it’s equal to the origin at every step.

我们创建两个队列。然后,我们从顶部字符向底部旋转移动,同时检查每一步是否等于原点。

boolean isRotationUsingQueue(String origin, String rotation) {
    if (origin.length() == rotation.length()) {
        return checkWithQueue(origin, rotation);
    }

    return false;
}

boolean checkWithQueue(String origin, String rotation) {
    if (origin.length() == rotation.length()) {
        Queue<Character> originQueue = getCharactersQueue(origin);

        Queue<Character> rotationQueue = getCharactersQueue(rotation);

        int k = rotation.length();
        while (k > 0 && null != rotationQueue.peek()) {
            k--;
            char ch = rotationQueue.peek();
            rotationQueue.remove();
            rotationQueue.add(ch);
            if (rotationQueue.equals(originQueue)) {
                return true;
            }
        }
    }

    return false;
}

Queue<Character> getCharactersQueue(String origin) {
    return origin.chars()
      .mapToObj(c -> (char) c)
      .collect(Collectors.toCollection(LinkedList::new));
}

6.2. Code Insight

6.2.代码洞察力

After creating the queue, what is relevant is how we can assert that our strings are equal:

创建队列后,我们要做的是如何断言我们的字符串是相等的:

int k = rotation.length();
while (k > 0 && null != rotationQueue.peek()) {
    k--;
    char ch = rotationQueue.peek();
    rotationQueue.remove();
    rotationQueue.add(ch);
    if (rotationQueue.equals(originQueue)) {
        return true;
    }
}

Moving the top element of the queue to the bottom in constant time gives us a new shifted object to compare with the origin.

在恒定时间内将队列顶端的元素移动到底部,就会得到一个新的移位对象,并与原点进行比较。

Let’s look at the algorithm complexity:

让我们来看看算法的复杂性:

  • Time Complexity: O(n*n) worst-case where we loop over the whole queue while comparing with the origin
  • Space Complexity: O(n)

6.3. Unit Tests

6.3.单元测试

Let’s test that using queues when shifting the rotation from the top to the tail will match the origin:

让我们来测试一下,当旋转从顶部移到尾部时,使用队列是否能与原点相匹配:

@Test
void givenOriginAndRotationInput_whenCheckingUsingQueues_thenIsRotation() {
    assertTrue(isRotationUsingQueue("abcd", "cdab"));
    assertTrue(isRotationUsingQueue("abcd", "abcd"));
}

Let’s test using queues when they won’t be equal:

让我们测试一下在队列不相等时使用队列的情况:

@Test
void givenOriginAndRotationInput_whenCheckingUsingQueues_thenNoRotation() {
    assertFalse(isRotationUsingQueue("abcd", "bbbb"));
    assertFalse(isRotationUsingQueue("abcd", "abcde"));
}

7. Conclusion

7.结论

In this article, we saw some algorithms to check whether a string is a rotation of another string. We saw how to search for common characters and assert equality using a doubled-origin string and the contains() string method. Similarly, we can use algorithms to check whether the remaining parts of the string match at shifted positions or using suffixes and prefixes. Finally, we also saw an example using queues and moving the peek to the tail of the rotation until it’s equal to the origin.

在本文中,我们了解了一些检查字符串是否是另一个字符串的轮换的算法。我们看到了如何使用双源字符串和 contains() 字符串方法搜索共同字符并断言相等。同样,我们可以使用算法来检查字符串的其余部分是否在移位位置或使用后缀和前缀时匹配。最后,我们还看到了一个使用队列和将窥视移动到旋转尾部直到与原点相等的示例。

As always, the code presented in this article is available over on GitHub.