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

时间:2019-04-24 06:00:56

标签: javascript string algorithm

我必须创建一个接受字符串的函数,并且该函数应根据输入是否包含重复的字符序列来返回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(),这使其运行缓慢。关于性能,是否有更好的解决方案?

13 个答案:

答案 0 :(得分:180)

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

  

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

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

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

要了解其工作原理,首先,假设一个字符串由字符串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是输入字符串的长度。

希望这会有所帮助!

答案 1 :(得分:66)

您可以通过capturing groupbackreference进行操作。只需检查它是第一个捕获值的重复即可。

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. ^$代表start and end anchors来预测位置。
  2. (.+)捕获任何模式并捕获值(\n除外)。
  3. \1是第一个捕获值的后向引用,\1+将检查捕获值的重复。

Regex explanation here

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

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

答案 2 :(得分:30)

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

  

此字符串的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"));

然后,您需要检查划分n的索引i。如果找到i这样的i+z[i]=n,则可以将字符串s压缩为长度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 :(得分: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 page,其中包含此页面上使用的算法。

答案 4 :(得分:17)

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

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

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

答案 5 :(得分: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

答案 6 :(得分:7)

用Python编写。我知道这不是平台,但是确实花了30分钟的时间。 P.S。=> 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")

答案 7 :(得分:6)

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

L:原始字符串的长度

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

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

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

答案 8 :(得分: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也被视为重复的, 因为它是两个重复的字符串ababab012012的和。

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

答案 9 :(得分: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

答案 10 :(得分:0)

这个问题发布已经一年多了,但我使用字符串的长度和对象形式来验证它是对还是错。

const check = (str) => {
  let count = 0;
  let obj = {};
  if (str.length < 2) return false;
  
  for(let i = 0; i < str.length; i++) {
    if (!obj[str[i]]) {
       count+=1;
      obj[str[i]] = 0;
    };
    obj[str[i]] = obj[str[i]] + 1;
  };
  
  if (Object.values(obj).every(item => item === 1)) {
    return false
  };
  
  if ([...str].length%count === 0) {
    return true
  } else {
    return false
  };
};

console.log(check("abcabcabcac")) // false
console.log(check("aaa")) // true
console.log(check("acaca")) // false
console.log(check("aa")) // true
console.log(check("abc")) // false
console.log(check("aabc")) // false

答案 11 :(得分:-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的答案。对于每个将长度分开的istr都是其前i个字符的重复,当且仅当在移动i个字符后它保持不变。

我想到这样的内置功能可能不可用或效率低下。在这种情况下,总是可以手动实现KMP algorithm,这所需的代码量与MBo答案中的算法大致相同。

答案 12 :(得分:-10)

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

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