Come posso verificare se una stringa è interamente realizzata con la stessa sottostringa?


128

Devo creare una funzione che accetta una stringa e dovrebbe restituire trueo falsebasarsi sul fatto che l'input sia costituito da una sequenza di caratteri ripetuta. La lunghezza della stringa data è sempre maggiore di 1e la sequenza di caratteri deve avere almeno una ripetizione.

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

Ho creato la seguente funzione:

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

Il controllo di questo fa parte del vero problema. Non posso permettermi una soluzione non efficiente come questa. Prima di tutto, scorre in mezzo alla metà della stringa.

Il secondo problema è che sta usando replace()in ogni ciclo che lo rende lento. Esiste una soluzione migliore per quanto riguarda le prestazioni?


19
Questo link potrebbe esserti utile. Trovo sempre geekforgeeks come una buona fonte di problemi di algoritmo - geeksforgeeks.org/…
Leron_says_get_back_Monica

9
Ti dispiace se prendo in prestito questo e lo trasformo in una sfida di programmazione sul sito di scambio di Programming Golf?
ouflak,

7
@ouflak puoi farlo.
Maheer Ali,

12
Nel caso in cui tu sia curioso, codegolf.stackexchange.com/questions/184682/…
ouflak

24
@Shidersz Usare le reti neurali per questo sembra un po 'come usare un cannone per sparare a una zanzara.
JAD

Risposte:


186

C'è un piccolo teorema elegante su stringhe come queste.

Una stringa è costituita dallo stesso modello ripetuto più volte se e solo se la stringa è una rotazione non banale di se stessa.

Qui, una rotazione significa eliminare un certo numero di caratteri dalla parte anteriore della stringa e spostarli verso la parte posteriore. Ad esempio, la stringa hellopotrebbe essere ruotata per formare una di queste stringhe:

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

Per capire perché questo funziona, innanzitutto, supponiamo che una stringa sia composta da k copie ripetute di una stringa w. Quindi, eliminare la prima copia del motivo ripetuto (w) dalla parte anteriore della stringa e incollarla sul retro restituirà la stessa stringa. La direzione inversa è un po 'più complicata da dimostrare, ma l'idea è che se si ruota una stringa e si torna indietro con ciò che si è iniziato, è possibile applicare ripetutamente quella rotazione per affiancare la stringa con più copie dello stesso modello (tale modello è il stringa necessaria per spostarsi alla fine per eseguire la rotazione).

Ora la domanda è come verificare se questo è il caso. Per questo, c'è un altro bellissimo teorema che possiamo usare:

Se xey sono stringhe della stessa lunghezza, allora x è una rotazione di y se e solo se x è una sottostringa di yy.

Ad esempio, possiamo vedere che lohelè una rotazione hellocome segue:

hellohello
   ^^^^^

Nel nostro caso, sappiamo che ogni stringa x sarà sempre una sottostringa di xx (apparirà due volte, una volta per ogni copia di x). Quindi, in sostanza, dobbiamo solo verificare se la nostra stringa x è una sottostringa di xx senza consentirne la corrispondenza al primo o a metà carattere. Ecco un one-liner per quello:

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

Supponendo che indexOfsia implementato usando un algoritmo di adattamento rapido delle stringhe, questo verrà eseguito nel tempo O (n), dove n è la lunghezza della stringa di input.

Spero che questo ti aiuti!


13
Molto bella! L'ho aggiunto alla pagina di riferimento jsPerf .
user42723

10
@ user42723 Cool! Sembra davvero, molto veloce.
templatetypedef

5
FYI: Ho avuto difficoltà a credere a quella frase fino a quando non ho invertito la dicitura: "Una stringa è una rotazione non banale di se stessa e se solo è costituita dallo stesso schema ripetuto più volte". Vai a capire.
Axel Podehl,

11
Hai riferimenti a quei teoremi?
HRK44

4
Penso che la prima affermazione sia la stessa di " Lemma 2.3 : se x e una rotazione di x sono uguali, allora x è una ripetizione" su doi.org/10.1016/j.tcs.2008.04.020 . Vedi anche: stackoverflow.com/a/2553533/1462295
BurnsBA

67

Puoi farlo da un gruppo di acquisizione e backreference . Basta controllare che sia la ripetizione del primo valore acquisito.

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

Nel precedente RegExp:

  1. ^e $sta per ancore di inizio e fine per prevedere la posizione.
  2. (.+)acquisisce qualsiasi modello e acquisisce il valore (tranne \n).
  3. \1è backreference del primo valore acquisito e \1+verificherebbe la ripetizione del valore acquisito.

Spiegazione Regex qui

Per il debug di RegExp usare: https://regex101.com/r/pqlAuP/1/debugger

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


2
Puoi spiegarci cosa sta facendo questa riga return /^(.+)\1+$/.test(str)
Thanveer Shah

34
Inoltre, qual è la complessità di questa soluzione? Non ne sono assolutamente sicuro, ma non sembra essere molto più veloce di quello dell'OP.
Leron_says_get_back_Monica

8
@PranavCBalan Non sono bravo con gli algoritmi, ecco perché scrivo nella sezione commenti. Tuttavia, ho diverse cose da menzionare: l'OP ha già una soluzione funzionante, quindi ne sta chiedendo una che gli darà prestazioni migliori e non hai spiegato come la tua soluzione supererà la sua. Più breve non significa più veloce. Inoltre, dal link che hai dato: If you use normal (TCS:no backreference, concatenation,alternation,Kleene star) regexp and regexp is already compiled then it's O(n).ma come hai scritto stai usando backreference, quindi è ancora O (n)?
Leron_says_get_back_Monica

5
È possibile utilizzare [\s\S]invece .se è necessario abbinare i caratteri di nuova riga allo stesso modo degli altri caratteri. Il carattere punto non corrisponde su newline; l'alternativa cerca tutti i caratteri spazi bianchi e non bianchi, il che significa che le nuove righe sono incluse nella corrispondenza. (Nota che questo è più veloce di quello più intuitivo (.|[\r\n]).) Tuttavia, se la stringa sicuramente non contiene newline, allora la semplice .sarà più veloce. Nota che sarà molto più semplice se il flag dotall è implementato.
Happy Dog

2
Non è /^(.+?)\1+$/un po 'più veloce? (12 passi contro 20 passi)
online Thomas

29

Forse l'approccio algoritmico più veloce sta costruendo una funzione Z in tempo lineare:

La funzione Z per questa stringa è un array di lunghezza n in cui l'i-esimo elemento è uguale al maggior numero di caratteri a partire dalla posizione i che coincidono con i primi caratteri di s.

In altre parole, z [i] è la lunghezza del prefisso comune più lungo tra se il suffisso di s che inizia con i.

Implementazione C ++ per riferimento:

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

Implementazione JavaScript
Aggiunte ottimizzazioni: creazione di metà di z-array e uscita anticipata

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

Quindi è necessario controllare gli indici iche dividono n. Se si trova tale iche i+z[i]=nla stringa spuò essere compresso alla lunghezza ie si può tornare true.

Ad esempio, per

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

z-array è

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

e possiamo trovarlo per

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

quindi spotrebbe essere rappresentato come sottostringa di lunghezza 4 ripetuta tre volte.


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

2
Grazie per aver aggiunto materiale JavaScript a Salman A e Pranav C Balan
MBo

1
Approccio alternativo evitando un'iterazione aggiuntivaconst 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'uso della funzione z è una buona idea, ma è "informazione pesante", contiene molte informazioni che non vengono mai utilizzate.
Axel Podehl,

@Axel Podehl Tuttavia, tratta la stringa nel tempo O (n) (ogni carattere viene usato al massimo due volte). In ogni caso dobbiamo controllare ogni carattere, quindi non esiste un algoritmo teoricamente più veloce (mentre i metodi integrati ottimizzati potrebbero sovraperformare). Anche nell'ultima modifica ho limitato il calcolo di 1/2 della lunghezza della stringa.
MBo

23

Ho letto la risposta di gnasher729 e l'ho implementata. L'idea è che se ci sono ripetizioni, allora ci deve essere (anche) un numero primo di ripetizioni.

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 algoritmo leggermente diverso è questo:

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
}

Ho aggiornato la pagina jsPerf che contiene gli algoritmi utilizzati in questa pagina.


Sembra molto veloce poiché salta i controlli non necessari.
Pranav C Balan,

1
Molto bello, solo penso che verificherei che la prima lettera si ripresenta nella posizione specificata prima di effettuare le chiamate di sottostringa.
Ben Voigt,

Per le persone che inciampano function*per la prima volta come me, è per dichiarare un generatore, non una funzione regolare. Vedi MDN
Julien Rousé,

17

Supponiamo che la stringa S abbia lunghezza N ed è composta da duplicati della sottostringa s, quindi la lunghezza di s divide N. Ad esempio, se S ha lunghezza 15, la sottostringa ha lunghezza 1, 3 o 5.

Sia S fatto di (p * q) copie di s. Quindi S è anche composto da p copie di (s, ripetute q volte). Abbiamo quindi due casi: Se N è primo o 1, allora S può essere fatto solo di copie della sottostringa di lunghezza 1. Se N è composito, allora abbiamo solo bisogno di controllare sottostringhe di lunghezza N / p per i numeri primi p che dividono la lunghezza di S.

Quindi determina N = la lunghezza di S, quindi trova tutti i suoi fattori primi nel tempo O (sqrt (N)). Se esiste solo un fattore N, controlla se S è la stessa stringa ripetuta N volte, altrimenti per ogni fattore primo p, controlla se S è costituito da ripetizioni p dei primi caratteri N / p.


Non ho controllato le altre soluzioni, ma questo sembra molto veloce. Puoi tralasciare la parte "Se c'è solo un fattore N, controlla ..., altrimenti" per semplicità, poiché questo non è un caso speciale. Sarebbe bello vedere un'implementazione Javascript che può essere eseguita in jsPerf accanto alle altre implementazioni.
user42723

1
Ora ho implementato questo nella mia risposta
user42723

10

Penso che anche una funzione ricorsiva potrebbe essere molto veloce. La prima osservazione è che la lunghezza massima ripetuta del motivo è la metà della lunghezza totale della stringa. E potremmo semplicemente testare tutte le possibili lunghezze ripetute del motivo: 1, 2, 3, ..., str.length / 2

La funzione ricorsiva è Test ripetuti (p, str) se questo schema viene ripetuto in str.

Se str è più lungo del modello, la ricorsione richiede che la prima parte (stessa lunghezza di p) sia una ripetizione, così come il resto di str. Quindi str viene effettivamente suddiviso in pezzi di lunghezza p.lunghezza.

Se il modello testato e lo str hanno le stesse dimensioni, la ricorsione finisce qui, con successo.

Se la lunghezza è diversa (accade per "aba" e modello "ab") o se i pezzi sono diversi, viene restituito falso, propagando la ricorsione.

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

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


1
Sarebbe più veloce controllare if( str===p.repeat(str.length/i) ) return true;invece di utilizzare una funzione ricorsiva?
Chronocidal,

1
Non inserire console.logs nei test jsperf, preparare le funzioni all'interno della sezione globals, preparare anche le stringhe di test nella sezione globals (mi dispiace, non posso modificare jsperf)
Salman A

@Salman - buon punto. Ho appena modificato jsperf dal mio predecessore (Pranav C), la prima volta che ho usato jsperf, un fantastico strumento.
Axel Podehl,

@SalmanA: aggiornato: jsperf.com/regex-and-loop/1 ... grazie per le informazioni ... anche non ho familiarità con esso (Jsperf) ... grazie per le informazioni
Pranav C Balan

Ciao Salman, grazie mille per jsperf.com/reegx-and-loop/10 - sì, quel nuovo test perf ha molto più senso. L'impostazione delle funzioni dovrebbe andare nel codice di preparazione.
Axel Podehl,

7

Scritto in Python. So che non è la piattaforma, ma ci sono voluti 30 minuti di tempo. 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

Il mio approccio è simile a gnasher729, in quanto utilizza la lunghezza potenziale della sottostringa come focus principale, ma è meno matematica e ad alta intensità di processo:

L: lunghezza della stringa originale

S: lunghezze potenziali di sottostringhe valide

Ciclo S da (parte intera di) L / 2 a 1. Se L / S è un numero intero, controlla la stringa originale con i caratteri S del pugno della stringa originale ripetuti L / S volte.

Il motivo del looping da L / 2 all'indietro e non da 1 in poi è quello di ottenere la sottostringa più grande possibile. Se si desidera il loop di sottostringa più piccolo possibile da 1 a L / 2. Esempio: "abababab" ha sia "ab" che "abab" come sottostringhe possibili. Quale dei due sarebbe più veloce se ti preoccupi solo di un risultato vero / falso dipende dal tipo di stringhe / sottostringhe a cui verrà applicato.


5

Il seguente codice Mathematica rileva quasi se l'elenco viene ripetuto almeno una volta. Se la stringa viene ripetuta almeno una volta, restituisce true, ma potrebbe anche restituire true se la stringa è una combinazione lineare di stringhe ripetute.

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

Questo codice cerca il contributo "full-length", che deve essere zero in una stringa ripetuta, ma la stringa accbbdè anche considerata ripetuta, in quanto è una somma delle due stringhe ripetute abababe 012012.

L'idea è di usare la trasformata di Fourier veloce e cercare gli spettri di frequenza. Osservando le altre frequenze, si dovrebbe essere in grado di rilevare anche questo strano scenario.


4

L'idea di base qui è quella di esaminare qualsiasi potenziale sottostringa, iniziando dalla lunghezza 1 e fermandosi alla metà della lunghezza della stringa originale. Esaminiamo solo le lunghezze di sottostringa che dividono uniformemente la lunghezza della stringa originale (ovvero str.length% substring.length == 0).

Questa implementazione esamina il primo carattere di ogni possibile iterazione della sottostringa prima di passare al secondo carattere, il che potrebbe far risparmiare tempo se si prevede che le sottostringhe siano lunghe. Se non viene rilevata alcuna discrepanza dopo aver esaminato l'intera sottostringa, restituiamo true.

Restituiamo falso quando esauriamo potenziali sottostringhe da controllare.

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

Non ho familiarità con JavaScript, quindi non so quanto sarà veloce, ma ecco una soluzione di tempo lineare (presupponendo un'implementazione incorporata ragionevole) usando solo i builtin. Descriverò l'algoritmo in pseudocodice.

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'idea è simile alla risposta di MBo. Per ognuno iche divide la lunghezza, strè una ripetizione dei suoi primi icaratteri se e solo se rimane lo stesso dopo aver cambiato ipersonaggio.

Mi viene in mente che un simile incasso potrebbe non essere disponibile o inefficiente. In questo caso, è sempre possibile implementare manualmente l' algoritmo KMP , che richiede la stessa quantità di codice dell'algoritmo nella risposta di MBo.


L'OP vuole sapere se esiste la ripetizione . La seconda riga di (il corpo di) la tua funzione conta il numero di ripetizioni - questo è il bit che deve essere spiegato. Ad esempio "abcabcabc" ha 3 ripetizioni di "abc", ma come ha funzionato la tua seconda riga se avesse ripetizioni?
Lawrence,

@Lawrence Non capisco la tua domanda. Questo algoritmo è basato sull'idea che la stringa è una ripetizione della sua stringa se e solo se per qualche divisore della sua lunghezza i, s[0:n-i] == s[i:n]o equivalentemente, s == s[i:n] + s[0:i]. Perché la seconda riga deve capire se avesse ripetizioni?
infmagic2047

Fammi vedere se capisco il tuo algoritmo. Innanzitutto, aggiungi stra se stesso il modulo t, quindi esegui la scansione tper cercare di trovare strall'interno t. Va bene, questo può funzionare (ho ritirato il mio voto negativo). Tuttavia, non è lineare in strlen (str). Dire che strè di lunghezza L. Quindi in ogni posizione p = 0,1,2, ..., controllando se str [0..L-1] == t [p..p + L-1] prende O (L ) tempo. Devi fare i controlli O (L) mentre attraversi i valori di p, quindi è O (L ^ 2).
Lawrence,

-10

Una delle idee semplici è quella di sostituire la stringa con la sottostringa di "" e se esiste del testo, allora è falso, altrimenti è vero.

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


sì, per abc o unicorno l'utente non verificherebbe con / abc / o / unicorn /, scusate se mi manca il contesto
Vinod kumar G

3
La domanda potrebbe essere più chiara, ma quello che sta chiedendo è un modo per decidere se la stringa è completamente composta da 2 o più ripetizioni di qualsiasi altra stringa. Non sta cercando una sottostringa specifica.
Happy Dog

2
Ho aggiunto alcuni chiarimenti alla domanda, che dovrebbe renderlo più chiaro ora.
Happy Dog

@Vinod se hai già intenzione di usare regex dovresti ancorare la tua partita e usare test. Nessun motivo per modificare la stringa solo per convalidare alcune condizioni.
Marie,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.