背景
在平时的编码练习中,我们经常会遇到与字符串匹配相关的问题,而这些问题的一个核心在于如何高效地操作字符串。最近,我在解决一个关于最短循环子串的问题时,发现通过 KMP(Knuth-Morris-Pratt)算法的前缀数组可以轻松解决这个问题。这篇博客希望从问题出发,讲解清晰的解题思路,并分享我的一些感想。
问题描述
给定一个字符串,判断该字符串是否是完全循环的,如果是,返回最短的循环子串;否则,返回空字符串。
- 输入:一个合法字符串,如
"abcabcabcabc"或"ab" - 输出:如果字符串是完全循环的,则输出其最短循环子串;否则,输出
""
例如:
- 输入
"abcabcabcabc",输出"abc" - 输入
"ab",输出""
思路分析
在解决这个问题之前,我们需要弄清楚几个重要概念:
-
什么是完全循环字符串? 一个字符串是完全循环的,意味着它可以通过一个较短的子串重复多次得到。例如:
"abcabcabcabc"是由"abc"重复 4 次组成。- 而
"ab"不是完全循环的。
-
如何找到字符串的周期? 我们可以使用字符串匹配算法(如 KMP)的 前缀数组 来高效确定字符串的周期。
KMP 前缀数组的作用
KMP 算法的前缀数组(又称部分匹配表)定义为:
prefix[i] 是字符串中前 i+1 个字符的前缀和后缀能够匹配的最大长度。
例如,对于字符串 "abcabcabcabc":
prefix[11] = 9,表示前 12 个字符中,前缀与后缀最大匹配长度是 9。
如果字符串是完全循环的,前缀数组的最大值可以帮助我们计算周期:
- 周期 =
n - prefix[n-1] - 若字符串长度
n能整除周期,则说明字符串是完全循环的。
实现步骤
- 计算前缀数组:遍历字符串,记录每个字符的前后缀最大匹配长度。
- 判断循环性:通过前缀数组的最大值计算周期,检查字符串是否为完全循环。
- 返回结果:如果是循环字符串,输出最短循环子串;否则,输出空字符串。
使用 KMP 算法解决最短循环子串问题:思路与感悟
背景
在平时的编码练习中,我们经常会遇到与字符串匹配相关的问题,而这些问题的一个核心在于如何高效地操作字符串。最近,我在解决一个关于最短循环子串的问题时,发现通过 KMP(Knuth-Morris-Pratt)算法的前缀数组可以轻松解决这个问题。这篇博客希望从问题出发,讲解清晰的解题思路,并分享我的一些感想。
问题描述
给定一个字符串,判断该字符串是否是完全循环的,如果是,返回最短的循环子串;否则,返回空字符串。
- 输入:一个合法字符串,如
"abcabcabcabc"或"ab" - 输出:如果字符串是完全循环的,则输出其最短循环子串;否则,输出
""
例如:
- 输入
"abcabcabcabc",输出"abc" - 输入
"ab",输出""
思路分析
在解决这个问题之前,我们需要弄清楚几个重要概念:
-
什么是完全循环字符串? 一个字符串是完全循环的,意味着它可以通过一个较短的子串重复多次得到。例如:
"abcabcabcabc"是由"abc"重复 4 次组成。- 而
"ab"不是完全循环的。
-
如何找到字符串的周期? 我们可以使用字符串匹配算法(如 KMP)的 前缀数组 来高效确定字符串的周期。
KMP 前缀数组的作用
KMP 算法的前缀数组(又称部分匹配表)定义为:
prefix[i] 是字符串中前 i+1 个字符的前缀和后缀能够匹配的最大长度。
例如,对于字符串 "abcabcabcabc":
prefix[11] = 9,表示前 12 个字符中,前缀与后缀最大匹配长度是 9。
如果字符串是完全循环的,前缀数组的最大值可以帮助我们计算周期:
- 周期 =
n - prefix[n-1] - 若字符串长度
n能整除周期,则说明字符串是完全循环的。
实现步骤
- 计算前缀数组:遍历字符串,记录每个字符的前后缀最大匹配长度。
- 判断循环性:通过前缀数组的最大值计算周期,检查字符串是否为完全循环。
- 返回结果:如果是循环字符串,输出最短循环子串;否则,输出空字符串。
代码实现
以下是完整的代码实现:
java
public class ShortestRepeatingSubstring {
public static String shortestRepeatingSubstring(String s) {
int n = s.length();
if (n == 0) return "";
// 计算前缀数组
int[] prefix = new int[n];
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && s.charAt(i) != s.charAt(j)) {
j = prefix[j - 1];
}
if (s.charAt(i) == s.charAt(j)) {
j++;
}
prefix[i] = j;
}
// 获取最大匹配长度
int k = prefix[n - 1];
// 判断是否完全循环
int period = n - k;
if (k > 0 && n % period == 0) {
return s.substring(0, period);
} else {
return "";
}
}
public static void main(String[] args) {
System.out.println(shortestRepeatingSubstring("abcabcabcabc")); // 输出: "abc"
System.out.println(shortestRepeatingSubstring("ab")); // 输出: ""
}
}
感想与总结
在解决这个问题时,我有以下几点收获:
- 数学与算法的结合: 通过前缀数组,我们不仅解决了字符串匹配的问题,还找到了字符串周期的数学性质。这种思维的结合让算法设计更具通用性。
- KMP 的巧妙之处: KMP 通常用于快速字符串匹配,而这里我们只是用它的前缀数组,便轻松解决了一个周期性问题,足见基础算法的重要性。
- 优化与高效性: 这套算法的时间复杂度是 O(n)O(n)O(n),空间复杂度也是 O(n)O(n)O(n)。对于超长字符串,它的效率是极高的。