字符串最小周期

来源:互联网 发布:cs1.5百度软件 编辑:程序博客网 时间:2024/05/17 04:37

1. 问题描述

给定一个长度为n的字符串S,如果存在一个字符串T,重复若干次T能够得到S,那么,S叫做周期串,T叫做S

的一个周期。如:字符串abababab是周期串,abab、ab都是它的周期,其中,ab是它的最小周期。设计一个算法,

计算S的最小周期。


2. 问题分析

首先我们会很容易想到最简单的暴力求解:假设字符串S的长度为 len,S的最小周期的长度为 i ,字符串S表示

为S[0…len-1]。

我们遍历 i 从1到 len/2 (只需要遍历到 len/2 即可,因为最小周期的长度不可能超过字符串S长度的一半),若len % 

i == 0,i 便有可能是最小周期的长度,具体是不是还需要进行判断。

判断 i 是不是最小周期的长度,要将字符串S后面的 len – i 个字符中每 i 个字符分成的各组字符分别与前 i 个字符

进行比较,即将 S[i…2i-1],S[2i…3i-1],……,S[len-i…len-1] 各个分组的每个字符分别与 S[0…i-1] 中的每个字符进

行比较。更简洁一点,省去分组的麻烦,直接将 S[i…len-1] 的每个字符 S[j] (j = i,…,len-1) 分别与 S[j % i] 进行比较即

可。

若比较的过程中出现字符不相等,则表示 i 并不是字符串S的周期长度,做 i++后判断转步骤判断是否是可能的周期,若是则转入步骤继续判断,否则继续迭代。若比较过程中每个字符判断均相等,则 i 就是字符串S的最小周期长度,停止进行判断。

若 i 遍历到 len/2 在步骤的比较过程中仍出现字符不相等,则说明字符串S不是周期串,此时返回最小周期长度

为0来表示S不是周期串。

暴力求解具体的C++代码如下:

#include <iostream>#include <cstring>using namespace std;int MinCycle(char *s){int i, j;int flag = 0;int slen = strlen(s);for (i = 1; i <= slen/2; i++){if (slen % i == 0){flag = 1;for (j = i; j < slen; j++){if (s[j] != s[j % i]){flag = 0;break;}}}if (flag == 1){return i;}}return 0;}
我们可以进行测试,输入s[] = "abccabccabcc",加上main函数如下:

int main(){char s[] = "abccabccabcc";int mclen = MinCycle(s);if (mclen == 0){cout << "字符串不是周期串";}else{cout << "字符串的最小周期是: "; for (int i = 0; i < mclen; i++){cout << s[i];}}cout << endl;return 0;}
运行输出结果为:


若把s改成s[] = "abcccabccabcc",运行输出结果为:


我们看到暴力求解用到两个 for 循环,时间复杂度可以认为是O(n^2),相对较高。


3. 优化解法

事实上有一个优化的解法,可将时间复杂度降为O(n)。我们先直接给出求解方法,再进行证明。

利用KMP算法中的next数组进行求解:

计算字符串S的 next 数组。此处计算 next 数组使用的是非优化的计算 next 的算法,并且要计算到 len,虽然并

不存在第 len号元素。

记k = next[len],mclen = len-k;若 len % mclen == 0 并且 mclen ≠ len,则 mclen 即为S的最小周期长度,前

 mclen个字符就是最小周期。

证明:

考察字符串S的k前缀S-front和k后缀S-back,S-front和S-back对应的元素是相同的,假设len % mclen == 0,记m =

 len/mclen,看下面的示意图,图中字符串表示框内标注的Sj (j = 1,2,3,…,m-2,m-1,m)表示整个字符串S的第j个分组,

当然所有分组的长度都为mclen。

对于S-front 的前 mclen 个字符S1和S-back 的前 mclen 个字符S2,可得出S1和S2的元素是相同的,我们用 S2 == S1表示;

对于S-front 的前 2*mclen 个字符S1+S2和S-back 的前 2*mclen 个字符S2+S3,由S2 == S1可得出S3 == S2;

对于S-front 的前 3*mclen 个字符S1+S2+S3和S-back 的前 3*mclen 个字符S2+S3+S4,由S2 == S1、S3 == S2可得出S4 == S3;

……

对于S-front 的前 (m-2)*mclen 个字符S1+S2+S3+…+Sm-2和S-back 的前 (m-2)*mclen 个字符S2+S3+S4+…+Sm-1,由S2 == S1、S3 == S2、S4 == S3、… 可得出Sm-1 == Sm-2;

对于S-front的前 (m-1)*mclen 个字符S1+S2+S3+…+Sm-1和S-back的前(m-1)*mclen个字符S2+S3+S4+…+Sm,由S2 == S1、S3 == S2、S4 == S3、…、Sm-1 == Sm-2可得出Sm == Sm-1。

根据以上分析,得出 Sm == Sm-1 == Sm-2 == … ==S3 == S2 == S1,即表示字符串S是由m个相同的子字符串组成的且长度都为 mclen。因此可以证明出 mclen 即为S的最小周期长度。

利用next求解字符串最小周期的C++代码如下:

#include <iostream>#include <cstring>using namespace std;void GetNext(char *s, int next[]){next[0] = -1;int k = -1;int j = 0;int slen = strlen(s);while (j < slen){if (k == -1 || s[j] == s[k]){++k;++j;next[j] = k;}else{k = next[k];}}}int MinCycle(char *s){const int num = 255;int next[num];GetNext(s, next);int len = strlen(s);int mclen = len - next[len];if (len % mclen == 0 && mclen != len){return mclen;}return 0;}
进行测试,加上main函数,输入s[] = "abccabccabcc"时,运行结果为:


s改成s[] = "abcccabccabcc",运行输出结果为:


可以看出和暴力求解得到的结果是一样的。利用 next 数组来求解只有在求解 next 数组用到一层循环,时间复杂度为

O(n),但是由于需要存储 next 数组,因而其空间复杂度上升了,这也就是典型的用空间来换时间。



1 0
原创粉丝点击