LC 796. 旋转字符串

题目描述

这是 LeetCode 上的 796. 旋转字符串 ,难度为 简单

给定两个字符串,sgoal。如果在若干次旋转操作之后,s 能变成 goal,那么返回 true

s 的旋转操作就是将 s 最左边的字符移动到最右边。

  • 例如, 若 s = 'abcde',在旋转一次之后结果就是'bcdea'

示例 1:

1
2
3
输入: s = "abcde", goal = "cdeab"

输出: true

示例 2:
1
2
3
输入: s = "abcde", goal = "abced"

输出: false

提示:

  • $1 <= s.length, goal.length <= 100$
  • sgoal 由小写英文字母组成

模拟

由于每次旋转操作都是将最左侧字符移动到最右侧,因此如果 goal 可由 s 经过多步旋转而来,那么 goal 必然会出现在 s + s 中,即满足 (s + s).contains(goal),同时为了 s 本身过长导致的结果成立,我们需要先确保两字符串长度相等。

代码:

1
2
3
4
5
class Solution {
public boolean rotateString(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}
}

  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(n)$

关于 contains 操作的复杂度说明

看到不少同学对 contains 的复杂度写成 $O(n)$ 有疑问。

在 Java 中,contains 实际最终是通过 indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) 来拿到子串在原串的下标,通过判断下标是否为 $-1$ 来得知子串是否在原串出现过。

我们知道一个较为普通的子串匹配算法的复杂度通为 $O(n*k)$,其中 $n$ 和 $k$ 分别是原串和子串的长度,而一些复杂度上较为优秀的算法可以做到 $O(n + k)$,例如 KMP

从复杂度上来看 KMP 似乎要更好,但实际上对于 indexOf 这一高频操作而言,KMP 的预处理逻辑和空间开销都是不可接受的。

因此在 OpenJDK 中的 indexOf 源码中,你看不到诸如 KMP 这一类「最坏情况下仍为线性复杂度」的算法实现。

但是 contains 的复杂度真的就是 $O(n * k)$ 吗?

其实并不是,这取决于 JVM 是否有针对 indexOf 的优化,在最为流行 HotSpot VM 中,就有对 indexOf 的优化。

使用以下两行命令执行 Main.java,会得到不同的用时。

1
2
3
4
5
6
7
8
9
10
11
12
// Main.java
import java.util.*;
class Main {
static String ss = "1900216589537958049456207450268985232242852754963049829410964867980510717200606495004259179775210762723370289106970649635773837906542900276476226929871813370344374628795427969854262816333971458418647697497933767559786473164055741512717436542961770628985635269208255141092673831132865";
static String pp = "830411595466023844647269831101019568881117264597716557501027220546437084223034983361631430958163646150071031688420479928498493050624766427709034028819288384316713084883575266906600102801186671777455503932259958027055697399984336592981698127456301551509241";
static int cnt = (int) 1e8;
static public void main(String[] args) {
long start = System.currentTimeMillis();
while (cnt-- > 0) ss.contains(pp);
System.out.println(System.currentTimeMillis() - start);
}
}

环境说明:

1
2
3
4
➜  java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

先执行 javac Main.java 进行编译后:

  1. 使用原始的 indexOf 实现进行匹配(执行多次,平均耗时为基准值 $X$):
    1
    java -XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_indexOf Main
  2. 使用 HotSpot VM 优化的 indexOf 进行匹配(执行多次,平均耗时为基准值 $X$ 的 $[0.55, 0.65]$ 之间):
    1
    java Main

因此实际运行的 contains 操作的复杂度为多少并不好确定,但可以确定是要优于 $O(n * k)$ 的。


最后

这是我们「刷穿 LeetCode」系列文章的第 No.796 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!