字符串最短循环子串 | 豆包MarsCode AI刷题

113 阅读5分钟

背景

在平时的编码练习中,我们经常会遇到与字符串匹配相关的问题,而这些问题的一个核心在于如何高效地操作字符串。最近,我在解决一个关于最短循环子串的问题时,发现通过 KMP(Knuth-Morris-Pratt)算法的前缀数组可以轻松解决这个问题。这篇博客希望从问题出发,讲解清晰的解题思路,并分享我的一些感想。


问题描述

给定一个字符串,判断该字符串是否是完全循环的,如果是,返回最短的循环子串;否则,返回空字符串。

  • 输入:一个合法字符串,如 "abcabcabcabc""ab"
  • 输出:如果字符串是完全循环的,则输出其最短循环子串;否则,输出 ""

例如:

  • 输入 "abcabcabcabc",输出 "abc"
  • 输入 "ab",输出 ""

思路分析

在解决这个问题之前,我们需要弄清楚几个重要概念:

  1. 什么是完全循环字符串? 一个字符串是完全循环的,意味着它可以通过一个较短的子串重复多次得到。例如:

    • "abcabcabcabc" 是由 "abc" 重复 4 次组成。
    • "ab" 不是完全循环的。
  2. 如何找到字符串的周期? 我们可以使用字符串匹配算法(如 KMP)的 前缀数组 来高效确定字符串的周期。


KMP 前缀数组的作用

KMP 算法的前缀数组(又称部分匹配表)定义为:
prefix[i] 是字符串中前 i+1 个字符的前缀和后缀能够匹配的最大长度。

例如,对于字符串 "abcabcabcabc"

  • prefix[11] = 9,表示前 12 个字符中,前缀与后缀最大匹配长度是 9。

如果字符串是完全循环的,前缀数组的最大值可以帮助我们计算周期:

  • 周期 = n - prefix[n-1]
  • 若字符串长度 n 能整除周期,则说明字符串是完全循环的。

实现步骤

  1. 计算前缀数组:遍历字符串,记录每个字符的前后缀最大匹配长度。
  2. 判断循环性:通过前缀数组的最大值计算周期,检查字符串是否为完全循环。
  3. 返回结果:如果是循环字符串,输出最短循环子串;否则,输出空字符串。

使用 KMP 算法解决最短循环子串问题:思路与感悟

背景

在平时的编码练习中,我们经常会遇到与字符串匹配相关的问题,而这些问题的一个核心在于如何高效地操作字符串。最近,我在解决一个关于最短循环子串的问题时,发现通过 KMP(Knuth-Morris-Pratt)算法的前缀数组可以轻松解决这个问题。这篇博客希望从问题出发,讲解清晰的解题思路,并分享我的一些感想。


问题描述

给定一个字符串,判断该字符串是否是完全循环的,如果是,返回最短的循环子串;否则,返回空字符串。

  • 输入:一个合法字符串,如 "abcabcabcabc""ab"
  • 输出:如果字符串是完全循环的,则输出其最短循环子串;否则,输出 ""

例如:

  • 输入 "abcabcabcabc",输出 "abc"
  • 输入 "ab",输出 ""

思路分析

在解决这个问题之前,我们需要弄清楚几个重要概念:

  1. 什么是完全循环字符串? 一个字符串是完全循环的,意味着它可以通过一个较短的子串重复多次得到。例如:

    • "abcabcabcabc" 是由 "abc" 重复 4 次组成。
    • "ab" 不是完全循环的。
  2. 如何找到字符串的周期? 我们可以使用字符串匹配算法(如 KMP)的 前缀数组 来高效确定字符串的周期。


KMP 前缀数组的作用

KMP 算法的前缀数组(又称部分匹配表)定义为:
prefix[i] 是字符串中前 i+1 个字符的前缀和后缀能够匹配的最大长度。

例如,对于字符串 "abcabcabcabc"

  • prefix[11] = 9,表示前 12 个字符中,前缀与后缀最大匹配长度是 9。

如果字符串是完全循环的,前缀数组的最大值可以帮助我们计算周期:

  • 周期 = n - prefix[n-1]
  • 若字符串长度 n 能整除周期,则说明字符串是完全循环的。

实现步骤

  1. 计算前缀数组:遍历字符串,记录每个字符的前后缀最大匹配长度。
  2. 判断循环性:通过前缀数组的最大值计算周期,检查字符串是否为完全循环。
  3. 返回结果:如果是循环字符串,输出最短循环子串;否则,输出空字符串。

代码实现

以下是完整的代码实现:

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")); // 输出: ""
    }
}

感想与总结

在解决这个问题时,我有以下几点收获:

  1. 数学与算法的结合: 通过前缀数组,我们不仅解决了字符串匹配的问题,还找到了字符串周期的数学性质。这种思维的结合让算法设计更具通用性。
  2. KMP 的巧妙之处: KMP 通常用于快速字符串匹配,而这里我们只是用它的前缀数组,便轻松解决了一个周期性问题,足见基础算法的重要性。
  3. 优化与高效性: 这套算法的时间复杂度是 O(n)O(n)O(n),空间复杂度也是 O(n)O(n)O(n)。对于超长字符串,它的效率是极高的。