如何检查字符串是否完全由相同的子字符串组成?


128

我必须创建一个接受字符串的函数,并且该函数应该返回truefalse基于输入是否包含重复的字符序列。给定字符串的长度始终大于,1并且字符序列必须至少重复一次。

"aa" // true(entirely contains two strings "a")
"aaa" //true(entirely contains three string "a")
"abcabcabc" //true(entirely containas three strings "abc")

"aba" //false(At least there should be two same substrings and nothing more)
"ababa" //false("ab" exists twice but "a" is extra so false)

我创建了以下功能:

function check(str){
  if(!(str.length && str.length - 1)) return false;
  let temp = '';
  for(let i = 0;i<=str.length/2;i++){
    temp += str[i]
    //console.log(str.replace(new RegExp(temp,"g"),''))
    if(!str.replace(new RegExp(temp,"g"),'')) return true;
  }
  return false;
}

console.log(check('aa')) //true
console.log(check('aaa')) //true
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false

对此进行检查是真正问题的一部分。我负担不起这样的无效解决方案。首先,它遍历字符串的一半。

第二个问题是它replace()在每个循环中使用,这使其运行缓慢。关于性能,是否有更好的解决方案?


19
该链接可能对您有用。我总是觉得geekforgeeks是解决算法问题的好方法-geeksforgeeks.org/…–
Leron_says_get_back_Monica

9
您介意我是否借用此书,并在Programming Golf交流网站上提出编码挑战?
ouflak

7
@ouflak,您可以这样做。
Maheer Ali

12

24
@Shidersz使用神经网络进行这种感觉有点像使用大炮射击蚊子。
JAD

Answers:


186

关于这样的字符串,有一个漂亮的小定理。

当且仅当字符串本身是非平凡的旋转时,字符串才由重复多次的相同模式组成。

在这里,旋转意味着从字符串的开头删除一些字符并将它们移到后面。例如,hello可以旋转字符串以形成以下任何字符串:

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

为了了解其工作原理,首先,假设一个字符串包含k个字符串w的k个重复副本。然后从字符串的开头删除重复图案(w)的第一份副本,然后将其粘贴到背面,将得到相同的字符串。相反的方向很难证明,但是其想法是,如果旋转字符串并返回开始的位置,则可以重复应用该旋转,以用相同模式的多个副本平铺字符串(该模式是您需要移至末尾进行旋转的字符串)。

现在的问题是如何检查是否是这种情况。为此,我们可以使用另一个漂亮的定理:

如果x和y是相同长度的字符串,则当且仅当x是yy的子字符串时,x是y的旋转。

作为示例,我们可以看到这lohelhello如下的轮换:

hellohello
   ^^^^^

在我们的例子中,我们知道每个字符串x始终是xx的子字符串(它将出现两次,在x的每个副本处出现一次)。因此,基本上我们只需要检查字符串x是否是xx的子字符串,而不允许它与第一个字符或中途字符匹配。这是一线的:

function check(str) {
    return (str + str).indexOf(str, 1) !== str.length;
}

假设 indexOf使用快速字符串匹配算法来实现,它将在时间O(n)中运行,其中n是输入字符串的长度。

希望这可以帮助!


13
非常好!我已将其添加到jsPerf基准测试页面。
user42723 '19

10
@ user42723太棒了!看起来真的非常快。
templatetypedef

5
仅供参考:我很难相信那句话,直到我颠倒了措辞:“当且仅当字符串包含重复多次的相同模式时,字符串本身才是不平凡的旋转”。去搞清楚。
Axel Podehl

11
您是否引用了这些定理?
HRK44

4
我认为第一个陈述与doi.org/10.1016/j.tcs.2008.04.020上的引理2.3:如果x和x的旋转相等,则x是重复” 相同。另请参阅:stackoverflow.com/a/2553533/1462295
BurnsBA,

67

您可以通过捕获组反向引用来实现。只需检查它是第一个捕获值的重复即可。

function check(str) {
  return /^(.+)\1+$/.test(str)
}

console.log(check('aa')) //true
console.log(check('aaa')) //true
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false

在上面的RegExp中:

  1. ^$代表开始和结束锚预测的位置。
  2. (.+)捕获任何模式并捕获值(除外\n)。
  3. \1是第一个捕获值的后向引用,\1+将检查捕获值的重复。

正则表达式的解释在这里

对于RegExp调试,请使用:https : //regex101.com/r/pqlAuP/1/debugger

性能:https : //jsperf.com/reegx-and-loop/13


2
你能向我们解释一下这条线是做回/^(.+)\1+$/.test(str)
Thanveer沙阿

34
另外,该解决方案的复杂性是什么?我不是很确定,但它似乎并不比OP的快很多。
Leron_says_get_back_Monica

8
@PranavCBalan我不擅长算法,这就是为什么我在评论部分中编写。但是我有几件事要提到-OP已经有一个可行的解决方案,因此他正在寻求一个可以给他带来更好性能的解决方案,并且您还没有解释您的解决方案将如何胜过他。更短并不意味着更快。另外,从您提供的链接中:If you use normal (TCS:no backreference, concatenation,alternation,Kleene star) regexp and regexp is already compiled then it's O(n).但是,正如您所写的,您正在使用向后引用,所以它仍然是O(n)吗?
Leron_says_get_back_Monica

5
如果您需要以与其他字符相同的方式匹配换行符[\s\S].则可以使用代替。点字符在换行符上不匹配;另一种方法是搜索所有空白和非空白字符,这意味着匹配中包括换行符。(请注意,这比更直观的操作要快(.|[\r\n])。)但是,如果字符串肯定不包含换行符,则简单操作.将最快。注意,如果实现了dotall标志,这将更加简单。
HappyDog

2
是不是/^(.+?)\1+$/快一点?(12步对20步)
在线Thomas

29

最快的算法方法也许是在线性时间内建立Z函数

此字符串的Z函数是一个长度为n的数组,其中第i个元素等于从与s的第一个字符重合的位置i开始的最大字符数。

换句话说,z [i]是s和s的后缀(从i开始)之间最长的公共前缀的长度。

C ++实现供参考:

vector<int> z_function(string s) {
    int n = (int) s.length();
    vector<int> z(n);
    for (int i = 1, l = 0, r = 0; i < n; ++i) {
        if (i <= r)
            z[i] = min (r - i + 1, z[i - l]);
        while (i + z[i] < n && s[z[i]] == s[i + z[i]])
            ++z[i];
        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }
    return z;
}

JavaScript实现
添加了优化-构建一半的z数组并提前退出

function z_function(s) {
  var n = s.length;
  var z = Array(n).fill(0);
  var i, l, r;
  //for our task we need only a half of z-array
  for (i = 1, l = 0, r = 0; i <= n/2; ++i) {
    if (i <= r)
      z[i] = Math.min(r - i + 1, z[i - l]);
    while (i + z[i] < n && s[z[i]] == s[i + z[i]])
      ++z[i];

      //we can check condition and return here
     if (z[i] + i === n && n % i === 0) return true;
    
    if (i + z[i] - 1 > r)
      l = i, r = i + z[i] - 1;
  }
  return false; 
  //return z.some((zi, i) => (i + zi) === n && n % i === 0);
}
console.log(z_function("abacabacabac"));
console.log(z_function("abcab"));

然后,您需要检查i除以n的索引。如果找到这样的字符串ii+z[i]=ns可以将其压缩为长度,i然后可以返回true

例如,对于

string s= 'abacabacabac'  with length n=12`

Z数组是

(0, 0, 1, 0, 8, 0, 1, 0, 4, 0, 1, 0)

我们可以找到

i=4
i+z[i] = 4 + 8 = 12 = n
and
n % i = 12 % 4 = 0`

因此s可能表示为长度4的子字符串,重复了三次。


3
return z.some((zi, i) => (i + zi) === n && n % i === 0)
Pranav C Balan

2
感谢您向Salman A和Pranav C Balan添加JavaScript内容
MBo

1
避免额外迭代的替代方法const check = (s) => { let n = s.length; let z = Array(n).fill(0); for (let i = 1, l = 0, r = 0; i < n; ++i) { if (i <= r) z[i] = Math.min(r - i + 1, z[i - l]); while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i]; // check condition here and return if (z[i] + i === n && n % i === 0) return true; if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1; } // or return false return false; }
Pranav C Balan

2
使用z函数是一个好主意,但这是“信息繁重”的,它包含许多从未使用过的信息。
Axel Podehl

@Axel Podehl不过,它会在O(n)时间内处理字符串(每个char最多使用两次)。无论如何,我们都必须检查每个字符,因此理论上不会有更快的算法(而优化的内置方法可能会胜过)。同样在上次编辑中,我将计算限制为字符串长度的1/2。
MBo

23

我阅读了gnasher729的答案并实现了它。这个想法是,如果有任何重复,那么就必须(也)有素数个重复。

function* primeFactors (n) {
    for (var k = 2; k*k <= n; k++) {
        if (n % k == 0) {
            yield k
            do {n /= k} while (n % k == 0)
        }
    }
    if (n > 1) yield n
}

function check (str) {
    var n = str.length
    primeloop:
    for (var p of primeFactors(n)) {
        var l = n/p
        var s = str.substring(0, l)
        for (var j=1; j<p; j++) {
            if (s != str.substring(l*j, l*(j+1))) continue primeloop
        }
        return true
    }
    return false
}

稍微不同的算法是这样的:

function check (str) {
    var n = str.length
    for (var p of primeFactors(n)) {
        var l = n/p
        if (str.substring(0, n-l) == str.substring(l)) return true
    }
    return false
}

我已经更新了jsPerf页面,其中包含该页面上使用的算法。


这似乎非常快,因为它跳过了不必要的检查。
Pranav C Balan

1
很好,只有我认为在进行子字符串调用之前,我会检查第一个字母是否再次出现在指定位置。
Ben Voigt

对于function*像我这样第一次绊脚的人,这是为了声明生成器,而不是常规函数。见MDN
朱利安·卢塞(JulienRousé)

17

假设字符串S的长度为N并且由子字符串s的重复项组成,则s的长度除以N。例如,如果S的长度为15,则子字符串的长度为1、3或5。

令S由s的(p * q)个副本组成。然后,S也由p的副本组成(s,重复q次)。因此,我们有两种情况:如果N为素数或1,则S只能由长度为1的子串的副本组成。如果N为复合的,则我们仅需检查长度为N / p的子串s以进行素数p除法S的长度。

因此,确定N = S的长度,然后在时间O(sqrt(N))中找到其所有主要因子。如果只有一个因子N,请检查S是否是同一字符串重复N次,否则,对于每个素数p,请检查S是否由前N / p个字符的p个重复组成。


我还没有检查其他解决方案,但这看起来非常快。为了简单起见,您可以省去“如果只有一个因子N,请检查...,否则”部分,因为这不是特殊情况。很高兴看到可以在jsPerf中与其他实现一起运行的Javascript实现。
–'user42723


10

我认为递归函数也可能非常快。第一个观察结果是最大重复图案长度是整个字符串的一半。我们可以测试所有可能的重复图案长度:1、2、3,...,str.length / 2

递归函数isRepeating(p,str)测试该模式是否在str中重复。

如果str大于模式,则递归要求第一部分(与p相同的长度)为重复部分,其余部分为str。因此,str有效地分解为长度为p.length的片段。

如果测试的模式和str大小相等,则递归将成功结束。

如果长度不同(表示“ aba”和模式“ ab”的情况),或者片段不同,则返回false,从而扩展递归。

function check(str)
{
  if( str.length==1 ) return true; // trivial case
  for( var i=1;i<=str.length/2;i++ ) { // biggest possible repeated pattern has length/2 characters

    if( str.length%i!=0 ) continue; // pattern of size i doesn't fit
    
    var p = str.substring(0, i);
    if( isRepeating(p,str) ) return true;
  }
  return false;
}


function isRepeating(p, str)
{
  if( str.length>p.length ) { // maybe more than 2 occurences

    var left = str.substring(0,p.length);
    var right = str.substring(p.length, str.length);
    return left===p && isRepeating(p,right);
  }
  return str===p; 
}

console.log(check('aa')) //true
console.log(check('aaa')) //true 
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false

性能:https : //jsperf.com/reegx-and-loop/13


1
检查if( str===p.repeat(str.length/i) ) return true;而不是使用递归函数会更快吗?
杀人事件'19

1
不要将console.logs放在jsperf测试中,不要在globals部分中准备函数,也不要在globals部分中准备测试字符串(对不起,不能编辑jsperf)
Salman A

@Salman-好点。我刚刚从我的前任(Pranav C)修改了jsperf,这是我第一次使用jsperf这个很棒的工具。
Axel Podehl,

@SalmanA:已更新:jsperf.com/regex-and-loop/1 ...感谢您提供的信息...即使我对此并不熟悉(Jsperf)...感谢提供的信息
Pranav C Balan

嗨Salman,非常感谢 jsperf.com/reegx-and-loop/10-是的,新的perf测试更有意义。功能的设置应包含在准备代码中。
Axel Podehl

7

用Python编写。我知道它不是平台,但是确实花了30分钟的时间。PS => PYTHON

def checkString(string):
    gap = 1 
    index= 0
    while index < len(string)/2:
        value  = [string[i:i+gap] for i in range(0,len(string),gap) ]

        x = [string[:gap]==eachVal for eachVal in value]

        if all(x):
            print("THEY ARE  EQUAL")
            break 

        gap = gap+1
        index= index+1 

checkString("aaeaaeaaeaae")

6

我的方法与gnasher729类似,因为它使用子字符串的潜在长度作为主要焦点,但是它的数学运算量和处理强度较低:

L:原始字符串的长度

S:有效子字符串的潜在长度

将S从L / 2的整数部分循环到1。如果L / S是整数,则将原始字符串与重复L / S次的原始字符串的第一个S个字符进行对照。

从L / 2向后而不是从1开始循环的原因是要获得最大的子串。如果要最小的子串循环,从1到L / 2。示例:“ abababab”具有“ ab”和“ abab”作为可能的子字符串。如果仅关心真/假结果,则两者中哪一个会更快,这取决于将应用于的字符串/子字符串的类型。


5

以下Mathematica代码几乎可以检测到该列表是否至少重复了一次。如果字符串重复至少一次,则返回true,但是如果字符串是重复字符串的线性组合,则也可能返回true。

IsRepeatedQ[list_] := Module[{n = Length@list},
   Round@N@Sum[list[[i]] Exp[2 Pi I i/n], {i, n}] == 0
];

该代码查找“全长”部分,在重复的字符串中该部分必须为零,但是该字符串accbbd也被视为重复的,因为它是两个重复的字符串ababab和的总和012012

这个想法是使用快速傅立叶变换,并寻找频谱。通过查看其他频率,人们也应该能够检测到这种奇怪的情况。


4

这里的基本思想是检查任何可能的子字符串,从长度1开始到终止于原始字符串长度的一半。我们仅查看将原始字符串长度平均划分的子字符串长度(即str.length%substring.length == 0)。

此实现在移动到第二个字符之前先检查每个可能的子字符串迭代的第一个字符,如果期望子字符串较长,则可以节省时间。如果在检查了整个子字符串后未发现不匹配,则返回true。

当我们用完潜在的子字符串进行检查时,我们返回false。

function check(str) {
  const len = str.length;
  for (let subl = 1; subl <= len/2; ++subl) {
    if ((len % subl != 0) || str[0] != str[subl])
      continue;
    
    let i = 1;
    for (; i < subl; ++i)
    {
      let j = 0;
      for (; j < len; j += subl)
        if (str[i] != str[j + i])
          break;
      if (j != len)
        break;
    }
    
    if (i == subl)
      return true;
  }
  return false;
}

console.log(check('aa')) //true
console.log(check('aaa')) //true
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false


-1

我不熟悉JavaScript,所以我不知道它的运行速度,但是这是一个仅使用内置函数的线性时间解决方案(假设合理的内置实现)。我将用伪代码描述该算法。

function check(str) {
    t = str + str;
    find all overlapping occurrences of str in t;
    for each occurrence at position i
        if (i > 0 && i < str.length && str.length % i == 0)
            return true;  // str is a repetition of its first i characters
    return false;
}

这个想法类似于MBo的答案。对于每个i分割长度的字符,当且仅当在转换字符后保持相同时,str才是其第一个i字符的重复i

我想到这样的内置函数可能不可用或效率低下。在这种情况下,始终可以手动实施KMP算法,该算法所花费的代码量与MBo答案中的算法相同。


OP想要知道是否存在重复。函数(函数的主体)的第二行计算重复次数-这是需要解释的位。例如,“ abcabcabc”有3个重复的“ abc”,但是您的第二行如何计算出是否有重复?
劳伦斯

@劳伦斯我不明白你的问题。这种算法是基于这样的理念,该字符串是其子的重复,当且仅当它的长度的一些因子is[0:n-i] == s[i:n]或者等价地,s == s[i:n] + s[0:i]。为什么第二行需要弄清楚是否有重复?
infmagic2047 '19

让我看看我是否了解您的算法。首先,你添加str到自己的形式t,然后扫描t,试图找到str里面t。好的,这可以解决问题(我收回了我的反对意见)。不过,它在strlen(str)中不是线性的。Say str的长度为L。然后在每个位置p = 0,1,2,...,检查str [0..L-1] == t [p..p + L-1]是否取O(L ) 时间。遍历p的值时需要进行O(L)检查,因此它是O(L ^ 2)。
劳伦斯

-10

一种简单的想法是用“”的子字符串替换该字符串,如果存在任何文本,则为假,否则为真。

'ababababa'.replace(/ab/gi,'')
"a" // return false
'abababab'.replace(/ab/gi,'')
 ""// return true


是的,对于abc或unicorn而言,用户不会使用/ abc /或/ unicorn /进行检查,如果我错过了您的情况,我们将感到抱歉
Vinod kumar G

3
这个问题可能更清楚,但是它要问的是一种确定字符串是否完全由2个或更多其他字符串重复组成的方法。它不搜索特定的子字符串。
HappyDog

2
我对这个问题做了一些澄清,现在应该使它更清楚。
HappyDog

@Vinod如果您已经要使用正则表达式,则应锚定匹配并使用测试。没有理由仅仅为了验证某些条件而修改字符串。
玛丽
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.