面试问题:检查一个字符串是否是另一个字符串的旋转。

235

我的朋友在软件开发职位的面试中被问到以下问题:

给定两个字符串 s1s2,你将如何检查 s1 是否是 旋转 版本的 s2

例如:

如果 s1 = "stackoverflow",则以下是它的一些旋转版本:

"tackoverflows"
"ackoverflowst"
"overflowstack"

"stackoverflwo" 不是旋转版本。

他给出的答案是:

s2 并查找最长的前缀,它是 s1 的子字符串,这将给你旋转点。一旦找到该点,请在该点处断开 s2,以获得 s2as2b,然后只需检查 concatenate(s2a,s2b) == s1 是否成立。

对我和我的朋友来说,这看起来像一个很好的解决方案。 但面试官并不这么认为。 他要求提供更简单的解决方案。 请帮我说明如何用 Java/C/C++ 实现此功能?

感谢您提前的帮助。


4
你不必检查concatenate(s2a,s2b)== s1,因为你知道s2a等于s1的开头部分。你只需要检查s2b是否等于s1从旋转点到结尾的子字符串即可。 - Jason Hall
33
这个问题和最佳回答是怎样获得那么多赞的?! - David Johnstone
9
@David:因为这很有趣。 - Cam
6
我会说,非常有趣且优雅简洁的回答。 - Guru
7
@David:因为这是一个以前没有在这里问过的问题,而且每个人理解(如果一个人不理解问题/答案,他通常肯定不会点赞;一个相当简单的问题有更广泛的受众),并且因为它被标记为Java和C两个标签。这很重要 :) - BalusC
显示剩余2条评论
26个回答

686

首先确保s1s2具有相同的长度。然后检查s2是否是s1连接s1后得到的子字符串:

algorithm checkRotation(string s1, string s2) 
  if( len(s1) != len(s2))
    return false
  if( substring(s2,concat(s1,s1))
    return true
  return false
end
在Java中:
boolean isRotation(String s1,String s2) {
    return (s1.length() == s2.length()) && ((s1+s1).indexOf(s2) != -1);
}

49
我喜欢它的优雅,但我必须思考一段时间以确保没有误报。(我认为没有误报。) - Jon Skeet
6
在Java中,您还可以使用(s1 + s1).contains(s2) - polygenelubricants
4
无论如何,我都认为这不是一个合适的面试问题。我认为它有一个“噢!”的成分。大多数程序员(包括我在内)会采用蛮力算法,这并不是不合理的,但可能会让面试官觉得不够“聪明”。 - Daniel Daranas
5
集中精力于s1+s1。显然,所有长度为s1.length的子字符串都是通过构造从s1旋转来的。因此,在s1+s1中作为子字符串并且长度为s1.length的任何字符串必须是s1的旋转。 - Daniel C. Sobral
6
这个解决方案的好处在于,一旦你指出来,就变得非常明显了。我很后悔自己没有想到它! - James B
显示剩余13条评论

101

毋庸置疑地,更好的答案应该是:“好吧,我会向stackoverflow社区提问,并且在5分钟内可能会得到至少4个非常好的答案”。大脑很棒,但我更看重那些知道如何与他人合作解决问题的人。


14
+1 纯粹是脸皮厚。让我开心了 :-) - Platinum Azure
5
如果他们有异议,那么你可以将这个问题链接给他们。 - Cam
51
在面试中掏出手机可能被视为不礼貌的行为,最终导致他们雇用Jon Skeet。 - tstenner
2
那实际上可能正是我会说的话。 - Chris Dutrow
6
我认为他们无法负担得起雇佣Jon Skeet。 - SolutionYogi
在我看来,这个答案没有意义。面试问题只是为了测试你是否能编写简单的算法,在未来,你可能会遇到足够专业化的问题,无法在stackoverflow上提问。 - Dhaivat Pandya

49

另一个基于THE答案的Python示例:

def isrotation(s1,s2):
     return len(s1)==len(s2) and s1 in 2*s2

1
有趣的是,我也想到了重复的s2而不是s1...然后意识到关系无论如何都是对称的。 - Matthieu M.
1
如果字符串可能很长,这里有一个使用Boyer-Moore算法的Python版本,可以获得O(n)的运行时间:def isrotation(s1, s2): return len(s1)==len(s2) and re.compile(re.escape(s1)).search(2*s2) is not None - Duncan
2
@Duncan:in 运算符不使用 O(n) 算法吗? - Ken Bloom
'in' 运算符的运行时间取决于两个字符串的长度。考虑 a in b,其中 axxxxxxz,而 b 则是很多 x :在最好的情况下,Python 将比较 len(b)-len(a) 个起始位置(我不确定这里,它可能只做 len(b) 次比较)和每个位置来自 len(a)-1 个字符的比较。另一方面,在带有常量前缀的正则表达式中,进行更少的比较(难以计算,但显然是 O(n),通常比较少)。 - Duncan
1
@Duncan:Python字符串方法使用了优化的Boyer-Moore-Horspool算法。我想知道Java是否有类似的优化。 - Thomas Ahle
1
@Thomas 谢谢你指出这一点。我曾经认为只有正则表达式使用Boyer-Moore算法,但现在我看到我错了。对于Python 2.4及更早版本,我的答案是正确的,但自从Python 2.5以来,s1 in s2已经被优化了。请参见http://effbot.org/zone/stringlib.htm以获取算法的描述。谷歌似乎表明Java没有快速的字符串搜索(例如,请参见http://johannburkard.de/software/stringsearch/),尽管我怀疑如果他们改变了它,它是否会破坏任何东西。 - Duncan

32

其他人已经提交了二次方最坏时间复杂度的解决方案,我会添加一个线性的解决方案(基于KMP算法):

bool is_rotation(const string& str1, const string& str2)
{
  if(str1.size()!=str2.size())
    return false;

  vector<size_t> prefixes(str1.size(), 0);
  for(size_t i=1, j=0; i<str1.size(); i++) {
    while(j>0 && str1[i]!=str1[j])
      j=prefixes[j-1];
    if(str1[i]==str1[j]) j++;
    prefixes[i]=j;
  }

  size_t i=0, j=0;
  for(; i<str2.size(); i++) {
    while(j>0 && str2[i]!=str1[j])
      j=prefixes[j-1];
    if(str2[i]==str1[j]) j++;
  }
  for(i=0; i<str2.size(); i++) {
    if(j>=str1.size()) return true;
    while(j>0 && str2[i]!=str1[j])
      j=prefixes[j-1];
    if(str2[i]==str1[j]) j++;
  }

  return false;
}

工作示例


5
+1 for ideone.com - 它看起来非常有趣! - MartyIX

25

编辑:如果你发现了更为优雅和高效的方式,请参考其他回答。我会采用一种暴力的方式。首先检查字符串长度,然后尝试每一个可能的旋转偏移量。如果没有一个可行的解决方案,则返回false-如果有一个可行的解决方案,则立即返回true。


没有特别需要连接字符串 - 只需使用指针(C语言)或索引(Java)并同时遍历两个字符串,一个是第一个字符串的开头,另一个是第二个字符串的当前候选旋转偏移量,并在必要时进行包装。在每个字符串位置处检查字符是否相等。如果到达第一个字符串的结尾,则完成。

这也可以很容易地通过连接来实现——至少在Java中效率可能会稍低些。


8
+1 - 我们不需要运行速度比最高效解决方案慢3倍以上的优雅解决方案。这是C语言... 微优化是必不可少的。 - Stephen C
8
面试官:说得头头是道,但我打赌这个人编不出代码。 - Humphrey Bogart
8
如果有人想这样想,他们可以向我索要代码。如果有人只是问我“如何做某事”,我通常会描述算法而不是直接给出代码。 - Jon Skeet
3
@Jon - 我认为Beau的评论是一个玩笑。 - oxbow_lakes
37
@Jon 这只是一个玩笑!面试官并不会面试 Jon Skeet,而是 Jon Skeet 面试他。 - Humphrey Bogart
显示剩余8条评论

17

这是一个只是为了好玩而使用正则表达式的示例:

boolean isRotation(String s1, String s2) {
   return (s1.length() == s2.length()) && (s1 + s2).matches("(.*)(.*)\\2\\1");
}

如果您可以使用一个特殊的分隔符,确保该字符不在任何字符串中出现,那么您可以使它更简单。

boolean isRotation(String s1, String s2) {
   // neither string can contain "="
   return (s1 + "=" + s2).matches("(.*)(.*)=\\2\\1");
}

你还可以使用有限重复的回顾后发:

boolean isRotation(String s1, String s2) {
   return (s1 + s2).matches(
      String.format("(.*)(.*)(?<=^.{%d})\\2\\1", s1.length())
   );
}

6
作为正则表达式专家,加一分。 - Chris Thornton
-1 因为在同一句话中使用了“regex”和“fun”这两个词,而没有用“not”修饰“fun”(开玩笑的,我没有给你点踩) - Binary Worrier
-3 表示正则表达式不好玩,这是错误的。 - manlycode
请问有谁能解释一下这个正则表达式 "(.)(.)=\2\1" 是如何工作的! - mawia

10

哇,为什么大家对一个 O(n^2) 的答案感到如此兴奋呢?我相信我们可以做得更好。上面的答案包括在 O(n) 循环中进行的 O(n) 操作(子字符串 / indexOf 调用)。即使使用更高效的搜索算法,如 Boyer-MooreKMP,最坏情况下还是会出现重复项,时间复杂度仍然是 O(n^2)

O(n) 的随机化解法非常简单;采用支持 O(1) 滑动窗口的哈希函数(例如 Rabin fingerprint),先对字符串1 进行哈希,再对字符串2 进行哈希,然后移动哈希函数1 的窗口,并检查是否有碰撞。

如果我们想象最坏的情况是扫描两个 DNA串,则可能发生碰撞的概率会增加,这可能会退化成类似于 O(n^(1+e)) 的复杂度(这只是猜测)。

最后,还有一种确定性的 O(nlogn) 解法,但外面有一个非常大的常数。基本思路是将两个字符串卷积起来。卷积的最大值就是两个字符串是否旋转的差异,使用 O(n) 检查即可确认。好处是如果有两个相等的最大值,则它们都是有效解。您可以用两个快速傅里叶变换和一个点乘以及一个反向快速傅里叶变换进行卷积,所需时间为:nlogn + nlogn + n + nlogn + n == O(nlogn)

由于不能填充零位,并且无法保证字符串长度为2^n,因此FFT将不是快速傅里叶变换,而是较慢的变换,仍然是O(nlogn),但常数比CT算法要大得多。

总之,我绝对、100%地相信这里有一种确定性的 O(n) 解决方案,但我找不到它。


KMP算法在对自身串进行拼接(无论是物理上还是虚拟上,使用%stringsize)时,保证具有线性时间复杂度。 - Kragen Javier Sitaker
+1 for Rabin-Karp。与KMP不同的是,它使用恒定的空间,并且更容易实现。(这也是我想到的第一个答案,在几秒钟内,使人难以看到“正确”的答案,因为这个答案就在那里,而且很棒。)你的卷积思路让我想起了Shor算法--我想知道是否有次线性量子解决方案--但现在这已经变得荒谬了,对吧? - Darius Bacon
1
RK算法无法提供确定性的O(n)解决方案,而KMP算法在空间复杂度上是O(n),这可能不太理想。可以查找Two Way或SMOA子字符串搜索算法,它们在时间复杂度上都是O(n),而且空间复杂度是O(1)。顺便说一句,glibc strstr函数使用的是Two Way算法,但如果你实际上将字符串连接起来使用,而不是使用%len,那么空间复杂度会退回到O(n)。 :-) - R.. GitHub STOP HELPING ICE

8

这是一个时间复杂度为O(n)且在原地执行的算法。它使用<运算符来比较字符串元素。当然这不是我的算法,我从这里找到了它(该网站为波兰语,我曾经在过去偶然发现过它,但现在无法用英文找到类似的内容,所以只能分享这个了 :))。

bool equiv_cyc(const string &u, const string &v)
{
    int n = u.length(), i = -1, j = -1, k;
    if (n != v.length()) return false;

    while( i<n-1 && j<n-1 )
    {
        k = 1;
        while(k<=n && u[(i+k)%n]==v[(j+k)%n]) k++;
        if (k>n) return true;
        if (u[(i+k)%n] > v[(j+k)%n]) i += k; else j += k;
    }
    return false;
}

+1... O(n) 从计算机科学的角度来看,比任何非 O(n) 的解决方案都更加深刻 :) - SyntaxT3rr0r
4
+1 表示支持一个在时间上最优且在代码大小(二进制和代码行数)上接近最优的解决方案。如果能够解释清楚就更好了。 - R.. GitHub STOP HELPING ICE
完全令人困惑。我们需要一个解释! - j_random_hacker

8
首先确保这两个字符串具有相同的长度。然后在C语言中,您可以通过简单的指针迭代来实现此操作。

int is_rotation(char* s1, char* s2)
{
  char *tmp1;
  char *tmp2;
  char *ref2;

  assert(s1 && s2);
  if ((s1 == s2) || (strcmp(s1, s2) == 0))
    return (1);
  if (strlen(s1) != strlen(s2))
    return (0);

  while (*s2)
    {
      tmp1 = s1;
      if ((ref2 = strchr(s2, *s1)) == NULL)
        return (0);
      tmp2 = ref2;
      while (*tmp1 && (*tmp1 == *tmp2))
        {
          ++tmp1;
          ++tmp2;
          if (*tmp2 == '\0')
            tmp2 = s2;
        }
      if (*tmp1 == '\0')
        return (1);
      else
        ++s2;
    }
  return (0);
}

19
啊,C语言。为什么要用一半的时间和代码来完成任务,当你可以用C语言做到同样的事情! - Humphrey Bogart
11
非常好的C语言代码。公平地说,这个问题标记为“c”。 - Nick Moore
5
在这段代码中,你至少在字符串长度和字符串比较两个地方遍历了2次,若不是3次。你可以省略这个检查,并将逻辑保留在循环中。当你进行循环时,如果一个字符串的字符数与另一个字符串不同,则退出循环。由于你知道起点并且知道什么时候遇到 null 终止符,所以你将知道这些字符串的长度。 - Nasko
12
@Beau Martinez - 因为有时候执行时间比开发时间更重要 :-) - phkahler
2
@phkahler - 问题在于它可能会更慢。其他语言中内置的索引函数通常使用快速字符串搜索算法,如Boyer-Moore、Rabin-Karp或Knuth-Morris-Pratt。仅仅在C语言中重新发明一切,并假设它更快是太天真了。 - Thomas Ahle

7

我猜最好用Java来做这件事情:

boolean isRotation(String s1,String s2) {
    return (s1.length() == s2.length()) && (s1+s1).contains(s2);
}

在Perl中,我会这样做:
sub isRotation {
 my($string1,$string2) = @_;
 return length($string1) == length($string2) && ($string1.$string1)=~/$string2/;
}

或者更好的方法是使用index函数而不是正则表达式:

sub isRotation {
 my($string1,$string2) = @_;
 return length($string1) == length($string2) && index($string2,$string1.$string1) != -1;
}

1
你在/\Q$string2/中忘记了\Q - Kragen Javier Sitaker
3
\Q会转义 $string2 中的任何特殊字符。如果不使用它,. 将被视为任意一个1个字符的字符串的旋转。 - jackrabbit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接