【问题标题】:How do I check if a string is entirely made of the same substring?如何检查字符串是否完全由相同的子字符串组成?
【发布时间】:2019-09-13 08:27:08
【问题描述】:

我必须创建一个接受字符串的函数,它应该根据输入是否包含重复的字符序列而返回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(),这使得它变慢了。在性能方面有更好的解决方案吗?

【问题讨论】:

  • 此链接可能对您有用。我总是发现 geekforgeeks 是解决算法问题的好来源 - geeksforgeeks.org/…
  • 您介意我借用这个并在 Programming Golf 交流网站上进行编码挑战吗?
  • @ouflak 你可以做到。
  • @Shidersz 为此使用神经网络感觉有点像用大炮射蚊子。

标签: javascript string algorithm


【解决方案1】:

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

一个字符串由重复多次的相同模式组成当且仅当该字符串是其自身的非平凡旋转。

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

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

要了解为什么会这样,首先,假设一个字符串由字符串 w 的 k 个重复副本组成。然后从字符串的前面删除重复模式 (w) 的第一个副本并将其粘贴到后面将返回相同的字符串。证明相反的方向有点棘手,但想法是,如果您旋转一个字符串并恢复您开始的状态,您可以重复应用该旋转以使用相同模式的多个副本平铺字符串(该模式是您需要移动到末尾才能进行旋转的字符串)。

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

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

例如,我们可以看到lohelhello 的旋转如下:

hellohello
   ^^^^^

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

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

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

希望这会有所帮助!

【讨论】:

  • 非常好!我已将其添加到 jsPerf benchmark 页面。
  • @user42723 酷!看起来真的非常快。
  • 仅供参考:我很难相信这句话,直到我颠倒了措辞:“当且仅当它由重复多次的相同模式组成时,字符串才是自身的非平凡旋转”。去图吧。
  • 你有参考这些定理吗?
  • 我认为第一个语句与 doi.org/10.1016/j.tcs.2008.04.020 处的“引理 2.3:如果 x 和 x 的旋转相等,则 x 是重复”相同。另见:stackoverflow.com/a/2553533/1462295
【解决方案2】:

您可以通过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

在上面的正则表达式中:

  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

【讨论】:

  • 你能给我们解释一下这条线在做什么 return /^(.+)\1+$/.test(str)
  • 这个解决方案的复杂性是什么?我不太确定,但它似乎并不比 OP 快多少。
  • @PranavCBalan 我不擅长算法,这就是我写在 cmets 部分的原因。但是我有几件事要提 - 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) 吗?
  • 如果您需要以与其他字符相同的方式匹配换行符,您可以使用[\s\S] 而不是.。点字符在换行符上不匹配;替代搜索所有空白和非空白字符,这意味着匹配中包含换行符。 (请注意,这比更直观的(.|[\r\n]) 更快。)但是,如果字符串肯定不包含换行符,那么简单的. 将是最快的。请注意,如果实现了the dotall flag,这将简单得多。
  • /^(.+?)\1+$/ 不是快一点吗? (12 步 vs 20 步)
【解决方案3】:

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

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

也就是说z[i]是最长公共前缀的长度 在 s 和从 i 开始的 s 的后缀之间。

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-array 并提前退出

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]=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 次。

【讨论】:

  • return z.some((zi, i) =&gt; (i + zi) === n &amp;&amp; n % i === 0)
  • 感谢您将 JavaScript 内容添加到 Salman A 和 Pranav C Balan
  • 通过避免额外迭代的替代方法const check = (s) =&gt; { let n = s.length; let z = Array(n).fill(0); for (let i = 1, l = 0, r = 0; i &lt; n; ++i) { if (i &lt;= r) z[i] = Math.min(r - i + 1, z[i - l]); while (i + z[i] &lt; n &amp;&amp; s[z[i]] == s[i + z[i]]) ++z[i]; // check condition here and return if (z[i] + i === n &amp;&amp; n % i === 0) return true; if (i + z[i] - 1 &gt; r) l = i, r = i + z[i] - 1; } // or return false return false; }
  • 使用z函数是个好主意,但它是“信息量大”,它包含很多从未使用过的信息。
  • @Axel Podehl 然而,它在 O(n) 时间内处理字符串(每个字符最多使用两次)。无论如何,我们必须检查每个字符,因此理论上没有更快的算法(而优化的内置方法可能会优于)。同样在最后一次编辑中,我将计算限制为字符串长度的 1/2。
【解决方案4】:

我阅读了 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

【讨论】:

  • 这似乎非常快,因为它跳过了不必要的检查。
  • 非常好,只是我想我会在进行子字符串调用之前检查第一个字母是否在指定位置再次出现。
  • 对于像我一样第一次发现function* 的人来说,这是为了声明一个生成器,而不是一个常规函数。见MDN
【解决方案5】:

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

让 S 由 s 的 (p*q) 个副本组成。那么 S 也由 (s, 重复 q 次) 的 p 个副本组成。因此我们有两种情况:如果 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 实现。
  • 我现在已经在my answer实现了这个
【解决方案6】:

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

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

如果 str 比模式长,则递归要求第一部分(与 p 长度相同)是重复的,以及 str 的其余部分。所以 str 被有效地分解为长度为 p.length 的片段。

如果测试的pattern和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

【讨论】:

  • 检查if( str===p.repeat(str.length/i) ) return true;会比使用递归函数更快吗?
  • 不要将console.logs放在jsperf测试中,在globals部分准备函数,在globals部分也准备测试字符串(对不起,无法编辑jsperf)
  • @Salman - 好点。我刚刚从我的前身(Pranav C)修改了 jsperf,我第一次使用 jsperf,很酷的工具。
  • @SalmanA :更新:jsperf.com/regex-and-loop/1 ...感谢您提供的信息...即使我不熟悉它(Jsperf)...感谢您提供的信息
  • 嗨 Salman,非常感谢 jsperf.com/reegx-and-loop/10 - 是的,新的性能测试更有意义。函数的设置应该进入编写代码。
【解决方案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")

【讨论】:

    【解决方案8】:

    我的方法与 gnasher729 类似,因为它使用子字符串的潜在长度作为主要关注点,但它的数学和过程密集度较低:

    L:原始字符串的长度

    S:有效子串的潜在长度

    从 L/2 的(整数部分)到 1 循环 S。如果 L/S 是整数,请检查原始字符串与原始字符串中重复 L/S 次的前 S 个字符。

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

    【讨论】:

      【解决方案9】:

      以下 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 两个重复字符串的总和。

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

      【讨论】:

        【解决方案10】:

        这里的基本思想是检查任何潜在的子字符串,从长度 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

        【讨论】:

          【解决方案11】:

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

          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

          【讨论】:

            【解决方案12】:

            我不熟悉 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 的答案中的算法所需的代码量大致相同。

            【讨论】:

            • OP 想知道重复是否存在。您的函数(主体)的第二行 counts 重复次数 - 这是需要解释的部分。例如。 "abcabcabc" 有 3 个重复的 "abc",但是您的第二行是如何确定是否它有任何重复的?
            • @Lawrence 我不明白你的问题。该算法基于这样的想法,即字符串是其子字符串的重复当且仅当对于其长度的某个除数is[0:n-i] == s[i:n] 或等效的s == s[i:n] + s[0:i]。为什么第二行要搞清楚有没有重复?
            • 让我看看我是否理解你的算法。首先,将str 附加到自身以形成t,然后扫描t 以尝试在t 中找到str。好的,这可以工作(我收回了我的反对票)。不过,它在 strlen(str) 中不是线性的。假设str 的长度为 L。然后在每个位置 p=0,1,2,...,检查 str[0..L-1] == t[p..p+L-1] 是否需要O(L) 时间。当你检查 p 的值时,你需要做 O(L) 检查,所以它是 O(L^2)。
            【解决方案13】:

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

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

            【讨论】:

            • 是的,对于 abc 或 unicorn,用户不会检查 /abc/ 或 /unicorn/ ,如果我错过了您的上下文,对不起
            • 这个问题可能更清楚,但它要求的是一种确定字符串是否完全由任何其他字符串的 2 个或更多重复组成的方法。它不是在搜索特定的子字符串。
            • 我已经对问题进行了一些澄清,现在应该更清楚了。
            • @Vinod 如果你已经打算使用正则表达式,你应该锚定你的匹配并使用测试。没有理由仅仅为了验证某些条件而修改字符串。
            猜你喜欢
            • 2021-10-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-12-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多