Comment vérifier si une chaîne est entièrement constituée de la même sous-chaîne?

128

Je dois créer une fonction qui prend une chaîne, et elle doit retourner trueou en falsefonction du fait que l'entrée consiste en une séquence de caractères répétée. La longueur de la chaîne donnée est toujours supérieure à 1et la séquence de caractères doit avoir au moins une répétition.

"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)

J'ai créé la fonction ci-dessous:

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

Vérifier cela fait partie du vrai problème. Je ne peux pas me permettre une solution non efficace comme celle-ci. Tout d'abord, il parcourt la moitié de la corde.

Le deuxième problème est qu'il utilise replace()dans chaque boucle, ce qui le ralentit. Existe-t-il une meilleure solution concernant les performances?

Maheer Ali
la source
19
Ce lien peut vous être utile. Je trouve toujours que les geekforgeeks sont une bonne source pour les problèmes d'algorithmes - geeksforgeeks.org
...
9
Cela vous dérange-t-il si j'emprunte ceci et en fait un défi de codage sur le site d'échange Programming Golf?
ouflak
7
@ouflak vous pouvez le faire.
Maheer Ali
12
Si vous
êtes
24
@Shidersz Utiliser les réseaux neuronaux pour cela, c'est un peu comme utiliser un canon pour tirer sur un moustique.
JAD

Réponses:

186

Il existe un petit théorème astucieux sur les cordes comme celles-ci.

Une chaîne se compose du même modèle répété plusieurs fois si et seulement si la chaîne est une rotation non triviale d'elle-même.

Ici, une rotation signifie supprimer un certain nombre de caractères de l'avant de la chaîne et les déplacer vers l'arrière. Par exemple, la chaîne hellopeut être tournée pour former l'une de ces chaînes:

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

Pour voir pourquoi cela fonctionne, supposons d'abord qu'une chaîne se compose de k copies répétées d'une chaîne w. Ensuite, en supprimant la première copie du motif répété (w) à l'avant de la corde et en la plaçant au dos, la même corde sera renvoyée. La direction inverse est un peu plus délicate à prouver, mais l'idée est que si vous faites pivoter une chaîne et récupérez ce avec quoi vous avez commencé, vous pouvez appliquer cette rotation à plusieurs reprises pour mettre en mosaïque la chaîne avec plusieurs copies du même motif (ce motif étant le chaîne dont vous aviez besoin pour aller à la fin pour faire la rotation).

Maintenant, la question est de savoir comment vérifier si tel est le cas. Pour cela, il existe un autre beau théorème que nous pouvons utiliser:

Si x et y sont des chaînes de même longueur, alors x est une rotation de y si et seulement si x est une sous-chaîne de yy.

À titre d'exemple, nous pouvons voir qu'il lohels'agit d'une rotation de la hellomanière suivante:

hellohello
   ^^^^^

Dans notre cas, nous savons que chaque chaîne x sera toujours une sous-chaîne de xx (elle apparaîtra deux fois, une fois à chaque copie de x). Donc, fondamentalement, nous avons juste besoin de vérifier si notre chaîne x est une sous-chaîne de xx sans lui permettre de correspondre au premier ou à mi-chemin. Voici un one-liner pour cela:

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

En supposant qu'il indexOfsoit implémenté à l'aide d'un algorithme de correspondance de chaîne rapide, cela s'exécutera au temps O (n), où n est la longueur de la chaîne d'entrée.

J'espère que cela t'aides!

templatetypedef
la source
13
Très agréable! Je l'ai ajouté à la page de référence jsPerf .
user42723
10
@ user42723 Cool! On dirait que c'est vraiment très rapide.
templatetypedef
5
FYI: J'ai eu du mal à croire cette phrase jusqu'à ce que j'inverse le libellé: "Une chaîne est une rotation non triviale d'elle-même si et seulement si elle consiste en le même motif répété plusieurs fois". Allez comprendre.
Axel Podehl
11
Avez-vous des références à ces théorèmes?
HRK44
4
Je pense que la première déclaration est la même que " Lemme 2.3 : Si x et une rotation de x sont égaux, alors x est une répétition" sur doi.org/10.1016/j.tcs.2008.04.020 . Voir aussi: stackoverflow.com/a/2553533/1462295
BurnsBA
67

Vous pouvez le faire par un groupe de capture et une référence arrière . Vérifiez simplement qu'il s'agit de la répétition de la première valeur capturée.

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

Dans le RegExp ci-dessus:

  1. ^et $représente les ancres de début et de fin pour prédire la position.
  2. (.+)capture n'importe quel motif et capture la valeur (sauf \n).
  3. \1est la référence arrière de la première valeur capturée et \1+vérifierait la répétition de la valeur capturée.

Explication Regex ici

Pour le débogage RegExp, utilisez: https://regex101.com/r/pqlAuP/1/debugger

Performances: https://jsperf.com/reegx-and-loop/13

Pranav C Balan
la source
2
Pouvez-vous nous expliquer ce que fait cette ligne return /^(.+)\1+$/.test(str)
Thanveer Shah
34
Quelle est également la complexité de cette solution? Je ne suis pas absolument sûr, mais cela ne semble pas être beaucoup plus rapide que celui du PO.
Leron_says_get_back_Monica
8
@PranavCBalan Je ne suis pas doué pour les algorithmes, c'est pourquoi j'écris dans la section commentaires. Cependant, j'ai plusieurs choses à mentionner - l'OP a déjà une solution fonctionnelle, il en demande donc une qui lui offrira de meilleures performances et vous n'avez pas expliqué comment votre solution surpassera la sienne. Plus court ne veut pas dire plus rapide. Aussi, à partir du lien que vous avez donné: If you use normal (TCS:no backreference, concatenation,alternation,Kleene star) regexp and regexp is already compiled then it's O(n).mais comme vous l'avez écrit, vous utilisez la référence arrière, est-ce toujours O (n)?
Leron_says_get_back_Monica
5
Vous pouvez utiliser à la [\s\S]place de .si vous avez besoin de faire correspondre les caractères de nouvelle ligne de la même manière que les autres caractères. Le caractère point ne correspond pas aux nouvelles lignes; l'alternative recherche tous les espaces blancs et non blancs, ce qui signifie que les retours à la ligne sont inclus dans la correspondance. (Notez que c'est plus rapide que le plus intuitif (.|[\r\n]).) Cependant, si la chaîne ne contient certainement pas de nouvelles lignes, alors le simple .sera le plus rapide. Notez que ce sera beaucoup plus simple si l'indicateur dotall est implémenté.
HappyDog
2
N'est-ce pas /^(.+?)\1+$/un peu plus rapide? (12 étapes vs 20 étapes)
ligne Thomas
29

L'approche algorithmique la plus rapide consiste peut-être à construire une fonction Z en temps linéaire:

La fonction Z de cette chaîne est un tableau de longueur n où le i-ème élément est égal au plus grand nombre de caractères à partir de la position i qui coïncide avec les premiers caractères de s.

En d'autres termes, z [i] est la longueur du plus long préfixe commun entre s et le suffixe de s commençant en i.

Implémentation C ++ pour référence:

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;
}

Implémentation JavaScript
Optimisations ajoutées - création de la moitié du z-array et sortie anticipée

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"));

Ensuite, vous devez vérifier les index iqui divisent n. Si vous trouvez tel ique i+z[i]=nla chaîne speut être compressée à la longueur iet vous pouvez retourner true.

Par exemple, pour

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

z-array est

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

et nous pouvons trouver que pour

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

ainsi spourrait être représenté comme sous-chaîne de longueur 4 répétée trois fois.

MBo
la source
3
return z.some((zi, i) => (i + zi) === n && n % i === 0)
Pranav C Balan
2
Merci d'avoir ajouté des éléments JavaScript à Salman A et Pranav C Balan
MBo
1
Approche alternative en évitant une itération supplémentaireconst 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
L'utilisation de la fonction z est une bonne idée, mais c'est une "information lourde", elle contient beaucoup d'informations qui ne sont jamais utilisées.
Axel Podehl
@Axel Podehl Néanmoins, il traite la chaîne en temps O (n) (chaque caractère est utilisé au plus deux fois). Dans tous les cas, nous devons vérifier chaque caractère, donc il n'y a pas d'algorithme théoriquement plus rapide (alors que les méthodes intégrées optimisées peuvent surpasser). Toujours dans la dernière édition, j'ai limité le calcul à 1/2 de la longueur de la chaîne.
MBo
23

J'ai lu la réponse de gnasher729 et l'ai implémentée. L'idée est que s'il y a des répétitions, alors il doit y avoir (aussi) un nombre premier de répétitions.

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
}

Un algorithme légèrement différent est le suivant:

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
}

J'ai mis à jour la page jsPerf qui contient les algorithmes utilisés sur cette page.

user42723
la source
Cela semble très rapide car il ignore les vérifications inutiles.
Pranav C Balan
1
Très bien, seulement je pense que je vérifierais que la première lettre se reproduit à l'emplacement spécifié avant de faire les appels de sous-chaîne.
Ben Voigt
Pour les gens qui trébuchent function*pour la première fois comme moi, c'est pour déclarer un générateur, pas une fonction ordinaire. Voir MDN
Julien Rousé
17

Supposons que la chaîne S a la longueur N et est composée de doublons de la sous-chaîne s, alors la longueur de s divise N. Par exemple, si S a la longueur 15, alors la sous-chaîne a la longueur 1, 3 ou 5.

Soit S constitué de (p * q) copies de s. Alors S est également constitué de p copies de (s, répétées q fois). On a donc deux cas: si N est premier ou 1, alors S ne peut être fait que de copies de la sous-chaîne de longueur 1. Si N est composite, alors il suffit de vérifier les sous-chaînes s de longueur N / p pour les nombres premiers p divisant la longueur de S.

Déterminez donc N = la longueur de S, puis trouvez tous ses facteurs premiers dans le temps O (sqrt (N)). S'il n'y a qu'un seul facteur N, vérifiez si S est la même chaîne répétée N fois, sinon pour chaque facteur premier p, vérifiez si S est constitué de p répétitions des N / p premiers caractères.

gnasher729
la source
Je n'ai pas vérifié les autres solutions, mais cela semble très rapide. Vous pouvez omettre la partie "S'il n'y a qu'un seul facteur N, cocher ..., sinon" pour simplifier, car ce n'est pas un cas particulier. Ce serait bien de voir une implémentation Javascript qui peut être exécutée dans jsPerf à côté des autres implémentations.
user42723
1
J'ai maintenant implémenté cela dans ma réponse
user42723
10

Je pense qu'une fonction récursive pourrait également être très rapide. La première observation est que la longueur maximale du motif répété est la moitié de la longueur totale de la chaîne. Et nous pourrions simplement tester toutes les longueurs de motifs répétés possibles: 1, 2, 3, ..., str.length / 2

La fonction récursive isRepeating (p, str) teste si ce modèle est répété dans str.

Si str est plus long que le motif, la récursivité nécessite que la première partie (même longueur que p) soit une répétition ainsi que le reste de str. Ainsi, str est effectivement divisé en morceaux de longueur p. Longueur.

Si le motif et la chaîne testés sont de taille égale, la récursivité se termine ici, avec succès.

Si la longueur est différente (se produit pour "aba" et le motif "ab") ou si les morceaux sont différents, alors false est renvoyé, propageant la récursivité.

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

Performances: https://jsperf.com/reegx-and-loop/13

Axel Podehl
la source
1
Serait-il plus rapide de vérifier if( str===p.repeat(str.length/i) ) return true;au lieu d'utiliser une fonction récursive?
Chronocidal
1
Ne mettez pas console.logs dans les tests jsperf, préparez les fonctions dans la section globals, préparez également les chaînes de test dans la section globals (désolé, impossible de modifier le jsperf)
Salman A
@Salman - bon point. Je viens de modifier le jsperf de mon prédécesseur (Pranav C), la première fois que j'ai utilisé jsperf, un outil sympa.
Axel Podehl
@SalmanA: mis à jour: jsperf.com/regex-and-loop/1 ... merci pour l'info ... même je ne suis pas familier avec elle (Jsperf) ... merci pour l'information
Pranav C Balan
Salut Salman, merci beaucoup pour jsperf.com/reegx-and-loop/10 - oui, ce nouveau test de perf a beaucoup plus de sens. La configuration des fonctions doit entrer dans le code de préparation.
Axel Podehl
7

A écrit ceci en Python. Je sais que ce n'est pas la plate-forme, mais cela a pris 30 minutes de temps. 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")
JustABeginner
la source
6

Mon approche est similaire à gnasher729, en ce sens qu'elle utilise la longueur potentielle de la sous-chaîne comme objectif principal, mais elle est moins gourmande en mathématiques et en processus:

L: longueur de la chaîne d'origine

S: longueurs potentielles des sous-chaînes valides

Boucle S de (partie entière de) L / 2 à 1. Si L / S est un entier, vérifiez votre chaîne d'origine par rapport aux premiers caractères S de la chaîne d'origine répétée L / S fois.

La raison de la boucle à partir de L / 2 vers l'arrière et non à partir de 1 est d'obtenir la plus grande sous-chaîne possible. Si vous voulez la plus petite boucle de sous-chaîne possible de 1 à L / 2. Exemple: "abababab" a à la fois "ab" et "abab" comme sous-chaînes possibles. Lequel des deux serait plus rapide si vous ne vous souciez que d'un résultat vrai / faux dépend du type de chaînes / sous-chaînes auxquelles il sera appliqué.

SunKnight0
la source
5

Le code Mathematica suivant détecte presque si la liste est répétée au moins une fois. Si la chaîne est répétée au moins une fois, elle renvoie true, mais elle peut également renvoyer true si la chaîne est une combinaison linéaire de chaînes répétées.

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

Ce code recherche la contribution «pleine longueur», qui doit être égale à zéro dans une chaîne répétitive, mais la chaîne accbbdest également considérée comme répétée, car il s'agit d'une somme des deux chaînes répétées abababet 012012.

L'idée est d'utiliser la transformée de Fourier rapide et de rechercher les spectres de fréquence. En regardant d'autres fréquences, on devrait également pouvoir détecter cet étrange scénario.

Par Alexandersson
la source
4

L'idée de base ici est d'examiner toute sous-chaîne potentielle, commençant à la longueur 1 et s'arrêtant à la moitié de la longueur de la chaîne d'origine. Nous examinons uniquement les longueurs de sous-chaîne qui divisent la longueur de la chaîne d'origine de manière égale (c'est-à-dire str.length% substring.length == 0).

Cette implémentation examine le premier caractère de chaque itération de sous-chaîne possible avant de passer au second caractère, ce qui peut gagner du temps si les sous-chaînes sont censées être longues. Si aucune incompatibilité n'est trouvée après avoir examiné la sous-chaîne entière, nous retournons true.

Nous retournons false lorsque nous manquons de sous-chaînes potentielles à vérifier.

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

Austin Mullins
la source
-1

Je ne suis pas familier avec JavaScript, donc je ne sais pas à quelle vitesse cela va être, mais voici une solution de temps linéaire (en supposant une implémentation intégrée raisonnable) utilisant uniquement des fonctions intégrées. Je vais décrire l'algorithme en pseudocode.

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;
}

L'idée est similaire à la réponse de MBo. Pour chacun iqui divise la longueur, strest une répétition de ses premiers icaractères si et seulement si elle reste la même après le décalage des icaractères.

Il me vient à l'esprit qu'un tel intégré peut être indisponible ou inefficace. Dans ce cas, il est toujours possible d'implémenter manuellement l' algorithme KMP , ce qui prend à peu près la même quantité de code que l'algorithme de la réponse de MBo.

infmagic2047
la source
Le PO veut savoir si la répétition existe . La deuxième ligne de (le corps de) votre fonction compte le nombre de répétitions - c'est le peu qui doit être expliqué. Par exemple, "abcabcabc" a 3 répétitions de "abc", mais comment votre deuxième ligne a-t- elle déterminé si elle avait des répétitions?
Lawrence
@Lawrence Je ne comprends pas votre question. Cet algorithme est basé sur l'idée que la chaîne est une répétition de son sous - chaîne si et seulement si pour quelque diviseur de sa longueur i, s[0:n-i] == s[i:n]ou équivalent, s == s[i:n] + s[0:i]. Pourquoi la deuxième ligne doit-elle déterminer si elle a eu des répétitions?
infmagic2047
Voyons si je comprends votre algorithme. Tout d'abord, vous vous ajoutez strà lui-même pour former t, puis scannez tpour essayer de trouver à l' strintérieur t. D'accord, cela peut fonctionner (j'ai retiré mon vote défavorable). Ce n'est pas linéaire dans strlen (str), cependant. Disons qu'il strest de longueur L. Puis à chaque position p = 0,1,2, ..., en vérifiant si str [0..L-1] == t [p..p + L-1] prend O (L ) temps. Vous devez faire des vérifications O (L) au fur et à mesure que vous parcourez les valeurs de p, donc c'est O (L ^ 2).
Lawrence
-10

Une des idées simples est de remplacer la chaîne par la sous-chaîne de "" et si un texte existe, alors il est faux, sinon c'est vrai.

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

Vinod kumar G
la source
oui, pour abc ou licorne, l'utilisateur ne vérifiera pas avec / abc / ou / unicorn /, désolé si je manque votre contexte
Vinod kumar G
3
La question pourrait être plus claire, mais ce qu'elle demande est un moyen de décider si la chaîne est complètement composée de 2 ou plusieurs répétitions d'une autre chaîne. Il ne recherche pas une sous-chaîne spécifique.
HappyDog
2
J'ai ajouté quelques précisions à la question, ce qui devrait la clarifier maintenant.
HappyDog
@Vinod si vous allez déjà utiliser regex, vous devez ancrer votre correspondance et utiliser test. Aucune raison de modifier la chaîne juste pour valider une condition.
Marie