Ripeti stringa - Javascript


271

Qual è il metodo migliore o più conciso per restituire una stringa ripetuta per un numero arbitrario di volte?

Di seguito è il mio scatto migliore finora:

function repeat(s, n){
    var a = [];
    while(a.length < n){
        a.push(s);
    }
    return a.join('');
}

5
Oltre 10 anni fa c'era una mia ben nota soluzione a questo problema, che ho usato come esempio in un articolo di ottimizzazione JavaScript un paio di mesi prima che tu ponessi questa domanda: webreference.com/programming/javascript/jkm3/3 .html Apparentemente, la maggior parte delle persone ha dimenticato quel codice e non vedo nessuna soluzione valida come la mia. L'algoritmo migliore sembra essere stato estratto dal mio codice; tranne che per un malinteso su come funziona il mio codice, esegue un ulteriore passaggio di concatenazione esponenziale che viene eliminato nel mio originale con un ciclo speciale.
Joseph Myers,

10
Nessuno ha sollevato la soluzione di Joseph. L'algoritmo ha 3700 anni. Il costo del passaggio aggiuntivo è trascurabile. E questo articolo contiene errori e idee sbagliate sulla concatenazione di stringhe in Javascript. Per chiunque sia interessato a come Javascript gestisce veramente le stringhe internamente, vedi Rope .
artistoex,

4
Nessuno sembra aver notato che la ripetizione del protoipo di String è definita e implementata, almeno in Firefox.
Kennebec,

3
@kennebec: Sì, questa è una funzione EcmaScript 6 che non esisteva quando è stata posta questa domanda. Ora è abbastanza ben supportato.
rvighne,

3
@rvighne - Proprio ora ho verificato kangax.github.io/compat-table/es6/#String.prototype.repeat Non considererei il supporto esclusivamente da Firefox e Chrome come "abbastanza ben supportato"
aaaaaa

Risposte:


406

Nota per i nuovi lettori: questa risposta è vecchia e non terribilmente pratica: è semplicemente "intelligente" perché usa le cose Array per fare cose String. Quando ho scritto "meno processo" intendevo sicuramente "meno codice" perché, come altri hanno notato nelle risposte successive, si comporta come un maiale. Quindi non usarlo se la velocità conta per te.

Metterei questa funzione direttamente sull'oggetto String. Invece di creare un array, riempirlo e unirlo con un carattere vuoto, basta creare un array della lunghezza corretta e unirlo con la stringa desiderata. Stesso risultato, meno processo!

String.prototype.repeat = function( num )
{
    return new Array( num + 1 ).join( this );
}

alert( "string to repeat\n".repeat( 4 ) );

36
Cerco di non estendere gli oggetti nativi, ma per il resto questa è una bella soluzione. Grazie!
Brad

34
@ Brad - perché no? Preferiresti inquinare lo spazio dei nomi globale con una funzione che ha una home abbastanza ben definita (l'oggetto String)?
Peter Bailey,

16
In realtà, entrambi i tuoi argomenti si applicano anche allo spazio dei nomi globale. Se ho intenzione di espandere uno spazio dei nomi e avere potenziali collisioni, preferirei farlo 1) non nel globale 2) in uno rilevante e 3) è facile da refactoring. Questo significa metterlo sul prototipo String, non in globale.
Peter Bailey,

11
una modifica che vorrei apportare a questa funzione sarebbe quella di mettere parseInt () attorno a "num", poiché se si dispone di una stringa numerica, si potrebbe ottenere un comportamento strano a causa della manipolazione del tipo di JS. ad esempio: "my string" .repeat ("6") == "61"
nickf

19
Se non si desidera estendere oggetti nativi, si potrebbe mettere la funzione in oggetto String, invece, in questo modo: String.repeat = function(string, num){ return new Array(parseInt(num) + 1).join(string); };. Chiamalo così:String.repeat('/\', 20)
Znarkus,

204

Ho testato le prestazioni di tutti gli approcci proposti.

Ecco la variante più veloce che ho.

String.prototype.repeat = function(count) {
    if (count < 1) return '';
    var result = '', pattern = this.valueOf();
    while (count > 1) {
        if (count & 1) result += pattern;
        count >>= 1, pattern += pattern;
    }
    return result + pattern;
};

O come funzione autonoma :

function repeat(pattern, count) {
    if (count < 1) return '';
    var result = '';
    while (count > 1) {
        if (count & 1) result += pattern;
        count >>= 1, pattern += pattern;
    }
    return result + pattern;
}

Si basa sull'algoritmo artistoex . È veramente veloce. E più grande è count, più veloce va rispetto new Array(count + 1).join(string)all'approccio tradizionale .

Ho cambiato solo 2 cose:

  1. sostituito pattern = thiscon pattern = this.valueOf()(cancella una conversione di tipo evidente);
  2. aggiunto if (count < 1)controllo da prototypejs all'inizio della funzione per escludere azioni non necessarie in quel caso.
  3. ottimizzazione applicata dalla risposta di Dennis (5-7% di accelerazione)

UPD

Creato un piccolo parco giochi per i test delle prestazioni qui per coloro che sono interessati.

variabile count~ 0 .. 100:

Diagramma delle prestazioni

costante count= 1024:

Diagramma delle prestazioni

Usalo e rendilo ancora più veloce se puoi :)


4
Bel lavoro! Penso che il count < 1caso sia un'ottimizzazione davvero inutile.
JayVee,

Ottimo algoritmo O (log N). Grazie per l'ottimizzazione con valueOf ()
vp_arth il

2
I collegamenti alle immagini sono morti.
Benjamin Gruenbaum,

I collegamenti vanno bene. Potrebbe essere indisponibilità temporanea
deturpata

Il test JSFiddle non funziona più correttamente; sembra continuare a eseguire la prima funzione più e più volte (per essere sicuro l'ha lasciata in funzione per mezz'ora)
RevanProdigalKnight

47

Questo problema è un noto / classico "problema di ottimizzazione" per JavaScript, causato dal fatto che le stringhe JavaScript sono "immutabili" e l'aggiunta mediante concatenazione anche di un singolo carattere in una stringa richiede la creazione, inclusa l'allocazione di memoria per e la copia in , un'intera nuova stringa.

Sfortunatamente, la risposta accettata in questa pagina è sbagliata, dove "errato" significa un fattore di prestazione di 3x per stringhe semplici di un carattere e 8x-97x per stringhe brevi ripetute più volte, a 300x per frasi ripetute e infinitamente sbagliato quando prendendo il limite dei rapporti di complessità degli algoritmi come nva all'infinito. Inoltre, c'è un'altra risposta in questa pagina che è quasi giusta (basata su una delle tante generazioni e varianti della soluzione corretta che circola su Internet negli ultimi 13 anni). Tuttavia, questa soluzione "quasi giusta" manca un punto chiave dell'algoritmo corretto causando un degrado delle prestazioni del 50%.

Risultati delle prestazioni JS per la risposta accettata, l'altra risposta con le migliori prestazioni (basata su una versione degradata dell'algoritmo originale in questa risposta) e questa risposta utilizzando il mio algoritmo creato 13 anni fa

~ Ottobre 2000 ho pubblicato un algoritmo per questo esatto problema che è stato ampiamente adattato, modificato, quindi alla fine poco compreso e dimenticato. Per porre rimedio a questo problema, nell'agosto 2008 ho pubblicato un articolo http://www.webreference.com/programming/javascript/jkm3/3.html che spiega l'algoritmo e lo utilizza come esempio di semplici ottimizzazioni JavaScript per scopi generici. Ormai Web Reference ha cancellato le mie informazioni di contatto e persino il mio nome da questo articolo. E ancora una volta, l'algoritmo è stato ampiamente adattato, modificato, quindi poco compreso e ampiamente dimenticato.

Algoritmo JavaScript di ripetizione / moltiplicazione di stringhe originale di Joseph Myers, circa Y2K come funzione di moltiplicazione del testo all'interno di Text.js; pubblicato agosto 2008 in questo modulo da Web Reference: http://www.webreference.com/programming/javascript/jkm3/3.html (L'articolo utilizzava la funzione come esempio di ottimizzazioni JavaScript, che è l'unica per gli strani nome "stringFill3.")

/*
 * Usage: stringFill3("abc", 2) == "abcabc"
 */

function stringFill3(x, n) {
    var s = '';
    for (;;) {
        if (n & 1) s += x;
        n >>= 1;
        if (n) x += x;
        else break;
    }
    return s;
}

Entro due mesi dalla pubblicazione di quell'articolo, questa stessa domanda fu posta su Stack Overflow e finì sotto il mio radar fino ad ora, quando apparentemente l'algoritmo originale per questo problema è stato nuovamente dimenticato. La migliore soluzione disponibile in questa pagina Stack Overflow è una versione modificata della mia soluzione, eventualmente separata da diverse generazioni. Sfortunatamente, le modifiche hanno rovinato l'ottimalità della soluzione. In effetti, cambiando la struttura del loop dal mio originale, la soluzione modificata esegue un ulteriore passaggio completamente non necessario di duplicazione esponenziale (unendo così la stringa più grande utilizzata nella risposta corretta con se stessa un tempo extra e quindi scartandola).

Di seguito segue una discussione di alcune ottimizzazioni JavaScript relative a tutte le risposte a questo problema e a beneficio di tutti.

Tecnica: evitare riferimenti a oggetti o proprietà degli oggetti

Per illustrare come funziona questa tecnica, utilizziamo una funzione JavaScript nella vita reale che crea stringhe di qualsiasi lunghezza sia necessaria. E come vedremo, è possibile aggiungere ulteriori ottimizzazioni!

Una funzione come quella qui usata è quella di creare un riempimento per allineare colonne di testo, per formattare denaro o per riempire i dati dei blocchi fino al limite. Una funzione di generazione del testo consente inoltre di inserire una lunghezza variabile per testare qualsiasi altra funzione che opera sul testo. Questa funzione è uno dei componenti importanti del modulo di elaborazione del testo JavaScript.

Man mano che procediamo, tratteremo altre due delle più importanti tecniche di ottimizzazione mentre svilupperemo il codice originale in un algoritmo ottimizzato per la creazione di stringhe. Il risultato finale è una funzione industriale e ad alte prestazioni che ho usato ovunque: allineare prezzi e totali degli articoli nei moduli d'ordine JavaScript, formattazione dei dati e formattazione di e-mail / messaggi di testo e molti altri usi.

Codice originale per la creazione di stringhe stringFill1()

function stringFill1(x, n) { 
    var s = ''; 
    while (s.length < n) s += x; 
    return s; 
} 
/* Example of output: stringFill1('x', 3) == 'xxx' */ 

La sintassi è qui è chiara. Come puoi vedere, abbiamo già utilizzato variabili di funzione locali, prima di passare a ulteriori ottimizzazioni.

Tenere presente che s.lengthnel codice è presente un riferimento innocente a una proprietà dell'oggetto che ne danneggia le prestazioni. Ancora peggio, l'uso di questa proprietà dell'oggetto riduce la semplicità del programma assumendo che il lettore sia a conoscenza delle proprietà degli oggetti stringa JavaScript.

L'uso di questa proprietà dell'oggetto distrugge la generalità del programma per computer. Il programma presuppone che xdeve essere una stringa di lunghezza uno. Ciò limita l'applicazione della stringFill1()funzione a tutto tranne che alla ripetizione di singoli caratteri. Anche i singoli caratteri non possono essere utilizzati se contengono più byte come l'entità HTML &nbsp;.

Il problema peggiore causato da questo uso non necessario di una proprietà dell'oggetto è che la funzione crea un ciclo infinito se testata su una stringa di input vuota x. Per verificare la generalità, applicare un programma alla minima quantità possibile di input. Un programma che si arresta in modo anomalo quando viene chiesto di superare la quantità di memoria disponibile ha una scusa. Un programma come questo che si arresta in modo anomalo quando viene chiesto di produrre nulla è inaccettabile. A volte il codice grazioso è un codice velenoso.

La semplicità può essere un obiettivo ambiguo della programmazione per computer, ma generalmente non lo è. Quando un programma manca di un ragionevole livello di generalità, non è valido dire "Il programma è abbastanza buono per quanto va". Come puoi vedere, l'uso della string.lengthproprietà impedisce a questo programma di funzionare in un'impostazione generale e, di fatto, il programma errato è pronto a causare un arresto del sistema o del browser.

C'è un modo per migliorare le prestazioni di questo JavaScript e prendersi cura di questi due gravi problemi?

Ovviamente. Usa solo numeri interi.

Codice ottimizzato per la creazione di stringhe stringFill2()

function stringFill2(x, n) { 
    var s = ''; 
    while (n-- > 0) s += x; 
    return s; 
} 

Codice temporale per confrontare stringFill1()estringFill2()

function testFill(functionToBeTested, outputSize) { 
    var i = 0, t0 = new Date(); 
    do { 
        functionToBeTested('x', outputSize); 
        t = new Date() - t0; 
        i++; 
    } while (t < 2000); 
    return t/i/1000; 
} 
seconds1 = testFill(stringFill1, 100); 
seconds2 = testFill(stringFill2, 100); 

Il successo finora stringFill2()

stringFill1()impiega 47.297 microsecondi (milionesimi di secondo) per riempire una stringa di 100 byte e stringFill2()impiega 27,68 microsecondi per fare la stessa cosa. Questo è quasi un raddoppio delle prestazioni evitando un riferimento a una proprietà dell'oggetto.

Tecnica: evitare di aggiungere stringhe corte a stringhe lunghe

Il nostro risultato precedente sembrava buono - molto buono, in effetti. La funzione migliorata stringFill2()è molto più veloce grazie all'uso delle nostre prime due ottimizzazioni. Ci crederesti se ti dicessi che può essere migliorato per essere molte volte più veloce di adesso?

Sì, possiamo raggiungere questo obiettivo. In questo momento dobbiamo spiegare come evitare di aggiungere stringhe brevi a stringhe lunghe.

Il comportamento a breve termine sembra essere abbastanza buono, rispetto alla nostra funzione originale. Agli informatici piace analizzare il "comportamento asintotico" di una funzione o un algoritmo di programma per computer, il che significa studiare il suo comportamento a lungo termine testandolo con input più ampi. A volte senza fare ulteriori test, non si diventa mai consapevoli dei modi per migliorare un programma per computer. Per vedere cosa accadrà, creeremo una stringa di 200 byte.

Il problema che si presenta con stringFill2()

Usando la nostra funzione di temporizzazione, scopriamo che il tempo aumenta a 62,54 microsecondi per una stringa di 200 byte, rispetto a 27,68 per una stringa di 100 byte. Sembra che il tempo dovrebbe essere raddoppiato per fare il doppio del lavoro, ma invece è triplicato o quadruplicato. Dall'esperienza di programmazione, questo risultato sembra strano, perché semmai la funzione dovrebbe essere leggermente più veloce poiché il lavoro viene svolto in modo più efficiente (200 byte per chiamata di funzione anziché 100 byte per chiamata di funzione). Questo problema ha a che fare con una proprietà insidiosa delle stringhe JavaScript: le stringhe JavaScript sono "immutabili".

Immutabile significa che non è possibile modificare una stringa una volta creata. Aggiungendo un byte alla volta, non stiamo consumando un altro byte di sforzo. Stiamo ricreando l'intera stringa più un altro byte.

In effetti, per aggiungere un altro byte a una stringa di 100 byte, sono necessari 101 byte di lavoro. Analizziamo brevemente il costo computazionale per la creazione di una stringa di Nbyte. Il costo per aggiungere il primo byte è 1 unità di sforzo computazionale. Il costo per aggiungere il secondo byte non è una unità ma 2 unità (copia del primo byte in un nuovo oggetto stringa e aggiunta del secondo byte). Il terzo byte richiede un costo di 3 unità, ecc.

C(N) = 1 + 2 + 3 + ... + N = N(N+1)/2 = O(N^2). Il simbolo O(N^2)è pronunciato Grande O di N al quadrato e significa che il costo computazionale a lungo termine è proporzionale al quadrato della lunghezza della stringa. Per creare 100 caratteri sono necessarie 10.000 unità di lavoro e per creare 200 caratteri sono necessarie 40.000 unità di lavoro.

Questo è il motivo per cui ci sono voluti più del doppio del tempo per creare 200 caratteri rispetto a 100 caratteri. In effetti, avrebbe dovuto impiegare quattro volte di più. La nostra esperienza di programmazione è stata corretta in quanto il lavoro viene svolto in modo leggermente più efficiente per stringhe più lunghe, e quindi ci è voluto solo circa tre volte di più. Quando l'overhead della chiamata di funzione diventa trascurabile su quanto tempo stiamo creando una stringa, in realtà ci vorrà quattro volte più tempo per creare una stringa il doppio.

(Nota storica: questa analisi non si applica necessariamente alle stringhe nel codice sorgente, ad esempio html = 'abcd\n' + 'efgh\n' + ... + 'xyz.\n'poiché il compilatore del codice sorgente JavaScript può unire le stringhe prima di trasformarle in un oggetto stringa JavaScript. Solo pochi anni fa, l'implementazione KJS di JavaScript si bloccherebbe o si arrestasse in modo anomalo durante il caricamento di lunghe stringhe di codice sorgente unite da segni più. Dal momento del calcolo O(N^2)non era difficile creare pagine Web che sovraccaricassero il browser Konqueror o Safari, che utilizzava il core del motore JavaScript KJS. mi sono imbattuto in questo problema quando stavo sviluppando un linguaggio di markup e un parser per il linguaggio di markup JavaScript, e poi ho scoperto cosa stava causando il problema quando ho scritto il mio script per JavaScript Include.)

Chiaramente questo rapido degrado delle prestazioni è un grosso problema. Come possiamo gestirlo, dato che non possiamo cambiare il modo di JavaScript di gestire le stringhe come oggetti immutabili? La soluzione è utilizzare un algoritmo che ricrea la stringa il meno volte possibile.

Per chiarire, il nostro obiettivo è evitare l'aggiunta di stringhe corte a stringhe lunghe, poiché per aggiungere la stringa corta, è necessario duplicare anche l'intera stringa lunga.

Come funziona l'algoritmo per evitare di aggiungere stringhe brevi a stringhe lunghe

Ecco un buon modo per ridurre il numero di volte in cui vengono creati nuovi oggetti stringa. Concatena lunghezze di stringa più lunghe insieme in modo che più di un byte alla volta venga aggiunto all'output.

Ad esempio, per creare una stringa di lunghezza N = 9:

x = 'x'; 
s = ''; 
s += x; /* Now s = 'x' */ 
x += x; /* Now x = 'xx' */ 
x += x; /* Now x = 'xxxx' */ 
x += x; /* Now x = 'xxxxxxxx' */ 
s += x; /* Now s = 'xxxxxxxxx' as desired */

Per fare ciò è necessario creare una stringa di lunghezza 1, creare una stringa di lunghezza 2, creare una stringa di lunghezza 4, creare una stringa di lunghezza 8 e infine creare una stringa di lunghezza 9. Quanto abbiamo risparmiato?

Vecchio costo C(9) = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 = 45.

Nuovo costo C(9) = 1 + 2 + 4 + 8 + 9 = 24.

Nota che abbiamo dovuto aggiungere una stringa di lunghezza 1 a una stringa di lunghezza 0, quindi una stringa di lunghezza 1 a una stringa di lunghezza 1, quindi una stringa di lunghezza 2 a una stringa di lunghezza 2, quindi una stringa di lunghezza 4 a una stringa di lunghezza 4, quindi a una stringa di lunghezza 8 a una stringa di lunghezza 1, al fine di ottenere una stringa di lunghezza 9. Ciò che stiamo facendo può essere riassunto evitando di aggiungere stringhe brevi a stringhe lunghe, o in altri parole, cercando di concatenare stringhe insieme di lunghezza uguale o quasi uguale.

Per il vecchio costo computazionale abbiamo trovato una formula N(N+1)/2. Esiste una formula per il nuovo costo? Sì, ma è complicato. L'importante è che lo sia O(N), e quindi raddoppiare la lunghezza della stringa raddoppierà approssimativamente la quantità di lavoro anziché quadruplicarla.

Il codice che implementa questa nuova idea è quasi complicato quanto la formula del costo computazionale. Quando lo leggi, ricorda che >>= 1significa spostare a destra di 1 byte. Quindi, se n = 10011è un numero binario, allora si n >>= 1ottiene il valore n = 1001.

L'altra parte del codice che potresti non riconoscere è l'operatore bit per bit, scritto &. L'espressione n & 1valuta vero se l'ultima cifra binaria di nè 1 e falsa se l'ultima cifra binaria di nè 0.

Nuova stringFill3()funzione altamente efficiente

function stringFill3(x, n) { 
    var s = ''; 
    for (;;) { 
        if (n & 1) s += x; 
        n >>= 1; 
        if (n) x += x; 
        else break; 
    } 
    return s; 
} 

Sembra brutto a un occhio non allenato, ma le sue prestazioni non sono altro che adorabili.

Vediamo quanto bene svolge questa funzione. Dopo aver visto i risultati, è probabile che non dimenticherai mai la differenza tra un O(N^2)algoritmo e un O(N)algoritmo.

stringFill1()impiega 88,7 microsecondi (milionesimi di secondo) per creare una stringa di 200 byte, stringFill2()richiede 62,54 e stringFill3()richiede solo 4,608. Cosa ha reso questo algoritmo molto meglio? Tutte le funzioni hanno sfruttato l'utilizzo delle variabili di funzione locali, ma sfruttando la seconda e la terza tecnica di ottimizzazione ha aggiunto un miglioramento di venti volte alle prestazioni di stringFill3().

Analisi più approfondite

Cosa rende questa particolare funzione spazzare via la concorrenza dall'acqua?

Come ho già detto, il motivo per cui entrambe queste funzioni stringFill1()e stringFill2(), eseguite così lentamente, è che le stringhe JavaScript sono immutabili. Non è possibile riallocare la memoria per consentire di aggiungere un altro byte alla volta ai dati della stringa memorizzati da JavaScript. Ogni volta che viene aggiunto un altro byte alla fine della stringa, l'intera stringa viene rigenerata dall'inizio alla fine.

Pertanto, al fine di migliorare le prestazioni dello script, è necessario pre-calcolare stringhe di lunghezza maggiore concatenando due stringhe insieme in anticipo e quindi costruendo ricorsivamente la lunghezza della stringa desiderata.

Ad esempio, per creare una stringa di byte di 16 lettere, per prima cosa viene precompilata una stringa di due byte. Quindi la stringa di due byte verrebbe riutilizzata per precomputare una stringa di quattro byte. Quindi la stringa di quattro byte verrebbe riutilizzata per precompilare una stringa di otto byte. Infine, due stringhe di otto byte verrebbero riutilizzate per creare la nuova stringa desiderata di 16 byte. Complessivamente sono state create quattro nuove stringhe, una di lunghezza 2, una di lunghezza 4, una di lunghezza 8 e una di lunghezza 16. Il costo totale è 2 + 4 + 8 + 16 = 30.

A lungo termine questa efficienza può essere calcolata aggiungendo in ordine inverso e utilizzando una serie geometrica che inizia con un primo termine a1 = N e con un rapporto comune di r = 1/2. La somma di una serie geometrica è data da a_1 / (1-r) = 2N.

Questo è più efficiente dell'aggiunta di un carattere per creare una nuova stringa di lunghezza 2, creando una nuova stringa di lunghezza 3, 4, 5 e così via, fino al 16. L'algoritmo precedente utilizzava quel processo di aggiunta di un singolo byte alla volta e il costo totale sarebbe n (n + 1) / 2 = 16 (17) / 2 = 8 (17) = 136.

Ovviamente, 136 è un numero molto maggiore di 30, e quindi l'algoritmo precedente impiega molto, molto più tempo per costruire una stringa.

Per confrontare i due metodi puoi vedere quanto più velocemente l'algoritmo ricorsivo (chiamato anche "divide and conquer") è su una stringa di lunghezza 123.457. Sul mio computer FreeBSD questo algoritmo, implementato nella stringFill3()funzione, crea la stringa in 0,001058 secondi, mentre la stringFill1()funzione originale crea la stringa in 0,0808 secondi. La nuova funzione è 76 volte più veloce.

La differenza di prestazioni aumenta con l'aumentare della lunghezza della stringa. Nel limite in cui vengono create stringhe sempre più grandi, la funzione originale si comporta approssimativamente come tempi C1(costanti) N^2e la nuova funzione si comporta come tempi C2(costanti) N.

Dal nostro esperimento possiamo determinare il valore di C1essere C1 = 0.0808 / (123457)2 = .00000000000530126997e il valore di C2essere C2 = 0.001058 / 123457 = .00000000856978543136. In 10 secondi, la nuova funzione potrebbe creare una stringa contenente 1.166.890.359 caratteri. Per creare questa stessa stringa, la vecchia funzione avrebbe bisogno di 7.218.384 secondi di tempo.

Sono quasi tre mesi rispetto a dieci secondi!

Sto solo rispondendo (con diversi anni di ritardo) perché la mia soluzione originale a questo problema sta fluttuando su Internet da più di 10 anni, e apparentemente è ancora poco compresa da pochi che se ne ricordano. Ho pensato che scrivendo un articolo a riguardo qui avrei aiutato:

Ottimizzazioni delle prestazioni per JavaScript / Pagina 3 ad alta velocità

Sfortunatamente, alcune delle altre soluzioni presentate qui sono ancora alcune di quelle che richiederebbero tre mesi per produrre la stessa quantità di output che una soluzione adeguata crea in 10 secondi.

Voglio prendere il tempo per riprodurre parte dell'articolo qui come una risposta canonica su Stack Overflow.

Si noti che l'algoritmo con le migliori prestazioni qui è chiaramente basato sul mio algoritmo ed è stato probabilmente ereditato dall'adattamento di qualcun altro di terza o quarta generazione. Sfortunatamente, le modifiche hanno portato a ridurne le prestazioni. La variazione della mia soluzione presentata qui forse non ha compreso la mia for (;;)espressione confusa che assomiglia al principale ciclo infinito di un server scritto in C e che è stato semplicemente progettato per consentire un'istruzione break posizionata con cura per il controllo del loop, il modo più compatto per evitare di replicare in modo esponenziale la stringa un ulteriore tempo inutile.


4
Questa risposta non avrebbe dovuto ricevere così tanti voti. Prima di tutto, le rivendicazioni di proprietà di Joseph sono ridicole. L'algoritmo sottostante ha 3700 anni.
artistoex,

2
In secondo luogo, contiene molta disinformazione. Le moderne implementazioni Javascript non toccano nemmeno il contenuto di una stringa quando si esegue la concatenazione (v8 rappresenta le stringhe concatenate come oggetto di tipo ConsString). Tutti i miglioramenti rimanenti sono trascurabili (in termini di complessità asintotica).
artistoex,

3
La tua idea di come vengono concatenate le stringhe è sbagliata. Per concatenare due stringhe, Javascript non legge affatto i byte delle stringhe costituenti. Invece, crea semplicemente un oggetto che si riferisce alle parti sinistra e destra. Questo è il motivo per cui l'ultima concatenazione nel loop non è più costosa delle prime.
artistoex,

3
Naturalmente, ciò comporta un costo maggiore di O (1) per l'indicizzazione della stringa, quindi la concatenazione può essere appiattita in seguito, il che merita davvero un'ulteriore valutazione.
artistoex,

1
Questa è stata una lettura eccellente. Dovresti scrivere un libro sull'efficienza e tutto il resto!

39

Questo è piuttosto efficiente

String.prototype.repeat = function(times){
    var result="";
    var pattern=this;
    while (times > 0) {
        if (times&1)
            result+=pattern;
        times>>=1;
        pattern+=pattern;
    }
    return result;
};

11
@Olegs, penso che l'idea di votare sia inferiore al voto per una persona o per la creatività di una persona (che in effetti è applaudibile), ma l'idea è quella di votare la soluzione più completa, in modo che possa essere facilmente trovata al in cima alla lista, senza dover leggere tutte le risposte nella ricerca di quella perfetta. (Perché, purtroppo, abbiamo tutti un tempo limitato ...)
Sorin Postelnicu,

38

Buone notizie! String.prototype.repeatfa ora parte di JavaScript .

"yo".repeat(2);
// returns: "yoyo"

Il metodo è supportato da tutti i principali browser, ad eccezione di Internet Explorer e Android Webview. Per un elenco aggiornato, vedere MDN: String.prototype.repeat> Compatibilità del browser .

MDN ha un polyfill per browser senza supporto.


Grazie a rapporti sullo stato attuale delle cose, anche se penso che il polyfill di Mozilla sia molto complicato per la maggior parte delle esigenze (presumo che provino a imitare il comportamento efficiente della loro implementazione in C) - quindi non risponderà davvero ai requisiti del PO per la concisione. Qualsiasi altro approccio impostato come polifill è destinato a essere più conciso ;-)
Guss,

2
Decisamente! Ma l'uso del built-in deve essere la versione più concisa. Poiché i polifill sono fondamentalmente solo back-port, tendono ad essere un po 'complessi per garantire la compatibilità con le specifiche (o le specifiche proposte, in questo caso). L'ho aggiunto per completezza, spetta all'OP decidere quale metodo utilizzare, immagino.
André Laszlo,


17

Espansione della soluzione di P.Bailey :

String.prototype.repeat = function(num) {
    return new Array(isNaN(num)? 1 : ++num).join(this);
    }

In questo modo dovresti essere al sicuro da tipi di argomenti imprevisti:

var foo = 'bar';
alert(foo.repeat(3));              // Will work, "barbarbar"
alert(foo.repeat('3'));            // Same as above
alert(foo.repeat(true));           // Same as foo.repeat(1)

alert(foo.repeat(0));              // This and all the following return an empty
alert(foo.repeat(false));          // string while not causing an exception
alert(foo.repeat(null));
alert(foo.repeat(undefined));
alert(foo.repeat({}));             // Object
alert(foo.repeat(function () {})); // Function

EDIT: crediti per jerone per la sua ++numidea elegante !


2
String.prototype.repeat = function(n){return new Array(isNaN(n) ? 1 : ++n).join(this);}
Ho

Ad ogni modo, secondo questo test ( jsperf.com/string-repeat/2 ), fare un semplice ciclo con concatenazione di stringhe sembra essere molto più veloce su Chrome rispetto all'utilizzo di Array.join. Non è divertente ?!
Marco Demaio,


5
/**  
@desc: repeat string  
@param: n - times  
@param: d - delimiter  
*/

String.prototype.repeat = function (n, d) {
    return --n ? this + (d || '') + this.repeat(n, d) : '' + this
};

questo è come ripetere più volte la stringa usando delimitatore.


4

Ecco un miglioramento del 5-7% sulla risposta di disfated.

Srotola il loop fermandoti a count > 1ed esegui un result += pattnernconcat aggiuntivo dopo il loop. Questo eviterà i loop finali precedentemente inutilizzati pattern += patternsenza dover usare un costoso if-check. Il risultato finale sarebbe simile al seguente:

String.prototype.repeat = function(count) {
    if (count < 1) return '';
    var result = '', pattern = this.valueOf();
    while (count > 1) {
        if (count & 1) result += pattern;
        count >>= 1, pattern += pattern;
    }
    result += pattern;
    return result;
};

Ed ecco il violino di disfated biforcuto per la versione non srotolata: http://jsfiddle.net/wsdfg/


2
function repeat(s, n) { var r=""; for (var a=0;a<n;a++) r+=s; return r;}

2
La concatenazione di stringhe non è costosa? Questo è almeno il caso in Java.
Vijay Dev,

Perché sì lo sono. Tuttavia, non può davvero essere ottimizzato in javarscript. :(
McTrafik

Che dire di questo improvvisatore di prestazioni: var r=s; for (var a=1;...:)))) Comunque secondo questo test ( jsperf.com/string-repeat/2 ) fare un semplice ciclo con concatenamento di stringhe come quello che hai suggerito sembra essere molto più veloce su Chrome rispetto all'uso di Array .aderire.
Marco Demaio,

@VijayDev - non secondo questo test: jsperf.com/ultimate-concat-vs-join
jbyrd

2

Test dei vari metodi:

var repeatMethods = {
    control: function (n,s) {
        /* all of these lines are common to all methods */
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        return '';
    },
    divideAndConquer:   function (n, s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        with(Math) { return arguments.callee(floor(n/2), s)+arguments.callee(ceil(n/2), s); }
    },
    linearRecurse: function (n,s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        return s+arguments.callee(--n, s);
    },
    newArray: function (n, s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        return (new Array(isNaN(n) ? 1 : ++n)).join(s);
    },
    fillAndJoin: function (n, s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        var ret = [];
        for (var i=0; i<n; i++)
            ret.push(s);
        return ret.join('');
    },
    concat: function (n,s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        var ret = '';
        for (var i=0; i<n; i++)
            ret+=s;
        return ret;
    },
    artistoex: function (n,s) {
        var result = '';
        while (n>0) {
            if (n&1) result+=s;
            n>>=1, s+=s;
        };
        return result;
    }
};
function testNum(len, dev) {
    with(Math) { return round(len+1+dev*(random()-0.5)); }
}
function testString(len, dev) {
    return (new Array(testNum(len, dev))).join(' ');
}
var testTime = 1000,
    tests = {
        biggie: { str: { len: 25, dev: 12 }, rep: {len: 200, dev: 50 } },
        smalls: { str: { len: 5, dev: 5}, rep: { len: 5, dev: 5 } }
    };
var testCount = 0;
var winnar = null;
var inflight = 0;
for (var methodName in repeatMethods) {
    var method = repeatMethods[methodName];
    for (var testName in tests) {
        testCount++;
        var test = tests[testName];
        var testId = methodName+':'+testName;
        var result = {
            id: testId,
            testParams: test
        }
        result.count=0;

        (function (result) {
            inflight++;
            setTimeout(function () {
                result.start = +new Date();
                while ((new Date() - result.start) < testTime) {
                    method(testNum(test.rep.len, test.rep.dev), testString(test.str.len, test.str.dev));
                    result.count++;
                }
                result.end = +new Date();
                result.rate = 1000*result.count/(result.end-result.start)
                console.log(result);
                if (winnar === null || winnar.rate < result.rate) winnar = result;
                inflight--;
                if (inflight==0) {
                    console.log('The winner: ');
                    console.log(winnar);
                }
            }, (100+testTime)*testCount);
        }(result));
    }
}

2

Ecco la versione sicura di JSLint

String.prototype.repeat = function (num) {
  var a = [];
  a.length = num << 0 + 1;
  return a.join(this);
};

2

Per tutti i browser

Questo è conciso quanto si ottiene:

function repeat(s, n) { return new Array(n+1).join(s); }

Se ti interessano anche le prestazioni, questo è un approccio molto migliore:

function repeat(s, n) { var a=[],i=0;for(;i<n;)a[i++]=s;return a.join(''); }

Se si desidera confrontare le prestazioni di entrambe le opzioni, vedere questo violino e questo violino per i test di riferimento. Durante i miei test, la seconda opzione era circa 2 volte più veloce in Firefox e circa 4 volte più veloce in Chrome!

Solo per browser moderni:

Nei browser moderni, ora puoi anche fare questo:

function repeat(s,n) { return s.repeat(n) };

Questa opzione non è solo più breve di entrambe le altre, ma è anche più veloce della seconda opzione.

Sfortunatamente, non funziona in nessuna versione di Internet Explorer. I numeri nella tabella specificano la prima versione del browser che supporta completamente il metodo:

inserisci qui la descrizione dell'immagine



2

Solo un'altra funzione di ripetizione:

function repeat(s, n) {
  var str = '';
  for (var i = 0; i < n; i++) {
    str += s;
  }
  return str;
}

2

ES2015è stato realizzato questo repeat()metodo!

http://www.ecma-international.org/ecma-262/6.0/#sec-string.prototype.repeat
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ String / repeat
http://www.w3schools.com/jsref/jsref_repeat.asp

/** 
 * str: String
 * count: Number
 */
const str = `hello repeat!\n`, count = 3;

let resultString = str.repeat(count);

console.log(`resultString = \n${resultString}`);
/*
resultString = 
hello repeat!
hello repeat!
hello repeat!
*/

({ toString: () => 'abc', repeat: String.prototype.repeat }).repeat(2);
// 'abcabc' (repeat() is a generic method)

// Examples

'abc'.repeat(0);    // ''
'abc'.repeat(1);    // 'abc'
'abc'.repeat(2);    // 'abcabc'
'abc'.repeat(3.5);  // 'abcabcabc' (count will be converted to integer)
// 'abc'.repeat(1/0);  // RangeError
// 'abc'.repeat(-1);   // RangeError


1

Questo può essere il più piccolo ricorsivo: -

String.prototype.repeat = function(n,s) {
s = s || ""
if(n>0) {
   s += this
   s = this.repeat(--n,s)
}
return s}


1

Concatenazione semplice ricorsiva

Volevo solo provarlo, e ho fatto questo:

function ditto( s, r, c ) {
    return c-- ? ditto( s, r += s, c ) : r;
}

ditto( "foo", "", 128 );

Non posso dire di averci pensato molto, e probabilmente mostra :-)

Questo è probabilmente migliore

String.prototype.ditto = function( c ) {
    return --c ? this + this.ditto( c ) : this;
};

"foo".ditto( 128 );

Ed è molto simile a una risposta già pubblicata - lo so.

Ma perché essere ricorsivo?

E un po 'di comportamento predefinito anche tu?

String.prototype.ditto = function() {
    var c = Number( arguments[ 0 ] ) || 2,
        r = this.valueOf();
    while ( --c ) {
        r += this;
    }
    return r;
}

"foo".ditto();

Perché , sebbene il metodo non ricorsivo gestirà ripetizioni arbitrariamente grandi senza colpire i limiti dello stack di chiamate, è molto più lento.

Perché mi sono preso la briga di aggiungere altri metodi meno intelligenti della metà di quelli già pubblicati?

In parte per mio divertimento, e in parte per sottolineare nel modo più semplice so che ci sono molti modi per scuoiare un gatto e, a seconda della situazione, è del tutto possibile che il metodo apparentemente migliore non sia l'ideale.

Un metodo relativamente veloce e sofisticato può effettivamente andare in crash e bruciare in determinate circostanze, mentre un metodo più lento e semplice può portare a termine il lavoro - alla fine.

Alcuni metodi possono essere poco più che exploit, e come tali inclini ad essere riparati dall'esistenza, e altri metodi possono funzionare meravigliosamente in tutte le condizioni, ma sono così costruiti che non si ha idea di come funzioni.

"E se non sapessi come funziona ?!"

Sul serio?

JavaScript soffre di uno dei suoi maggiori punti di forza; è altamente tollerante nei confronti dei cattivi comportamenti e così flessibile che si piegherà all'indietro per restituire risultati, quando sarebbe potuto essere migliore per tutti se fosse scattato!

"Da un grande potere derivano grandi responsabilità" ;-)

Ma più seriamente e, soprattutto, anche se domande generali come questa portano a una bellezza sotto forma di risposte intelligenti che, se non altro, ampliano le conoscenze e gli orizzonti, alla fine, il compito a portata di mano - lo script pratico che utilizza il metodo risultante - potrebbe richiedere un po 'meno o un po' più intelligente di quanto suggerito.

Questi algoritmi "perfetti" sono divertenti e tutti, ma "una taglia unica" raramente, se non mai, sarà migliore di quelli fatti su misura.

Questo sermone ti è stato offerto per mancanza di sonno e un interesse passeggero. Vai avanti e codifica!


1

In primo luogo, le domande del PO sembrano riguardare la concisione - che intendo significhi "semplice e facile da leggere", mentre la maggior parte delle risposte sembrano riguardare l'efficienza - che ovviamente non è la stessa cosa e penso anche che, a meno che non si attui molto specifici algoritmi di manipolazione di dati di grandi dimensioni, non dovrebbero preoccuparti quando si arriva a implementare funzioni Javascript di manipolazione dei dati di base. La concisione è molto più importante.

In secondo luogo, come ha osservato André Laszlo, String.repeat fa parte di ECMAScript 6 ed è già disponibile in diverse implementazioni popolari - quindi l'implementazione più concisa String.repeatnon è quella di implementarla ;-)

Infine, se è necessario supportare host che non offrono l'implementazione di ECMAScript 6, il polyfill di MDN citato da André Laszlo è tutt'altro che conciso.

Quindi, senza ulteriori indugi, ecco il mio conciso polyfill:

String.prototype.repeat = String.prototype.repeat || function(n){
    return n<=1 ? this : this.concat(this.repeat(n-1));
}

Sì, questa è una ricorsione. Mi piacciono le ricorsioni: sono semplici e se eseguite correttamente sono facili da capire. Per quanto riguarda l'efficienza, se la lingua lo supporta possono essere molto efficienti se scritti correttamente.

Dai miei test, questo metodo è ~ 60% più veloce Array.joindell'approccio. Anche se ovviamente non arriva da nessuna parte l'implementazione sfigurata, è molto più semplice di entrambi.

La mia configurazione di test è il nodo v0.10, usando "Modalità rigorosa" (penso che abiliti una sorta di TCO ), chiamando repeat(1000)una stringa di 10 caratteri un milione di volte.


1

Se ritieni che tutte queste definizioni di prototipi, creazioni di array e operazioni di join siano eccessive, utilizza un codice a riga singola dove ne hai bisogno. Stringa S che ripete N volte:

for (var i = 0, result = ''; i < N; i++) result += S;

3
Il codice dovrebbe essere leggibile. Se lo utilizzerai letteralmente solo una volta, formattalo correttamente (o usa il Array(N + 1).join(str)metodo se non è un collo di bottiglia delle prestazioni). Se c'è la minima possibilità di usarlo due volte, spostalo su una funzione con un nome appropriato.
cloudfeet,

1

Usa Lodash per la funzionalità dell'utilità Javascript, come ripetere le stringhe.

Lodash offre buone prestazioni e compatibilità ECMAScript.

Lo consiglio vivamente per lo sviluppo dell'interfaccia utente e funziona anche sul lato server.

Ecco come ripetere la stringa "yo" 2 volte usando Lodash:

> _.repeat('yo', 2)
"yoyo"

0

Soluzione ricorsiva usando divide and conquer:

function repeat(n, s) {
    if (n==0) return '';
    if (n==1 || isNaN(n)) return s;
    with(Math) { return repeat(floor(n/2), s)+repeat(ceil(n/2), s); }
}

0

Sono venuto qui in modo casuale e non ho mai avuto motivo di ripetere un carattere in javascript prima.

Sono stato colpito dal modo di farlo di artistoex e dai risultati sfigurati. Ho notato che l'ultimo concat con archi non era necessario, come ha anche sottolineato Dennis.

Ho notato alcune altre cose giocando con il campionamento sfigurato messo insieme.

I risultati variavano abbastanza spesso favorendo l'ultima corsa e algoritmi simili sarebbero spesso jockey per posizione. Una delle cose che ho cambiato era invece di usare il conteggio generato da JSLitmus come seed per le chiamate; poiché il conteggio è stato generato in modo diverso per i vari metodi, ho inserito un indice. Questo ha reso la cosa molto più affidabile. Ho quindi cercato di garantire che le stringhe di dimensioni variabili fossero passate alle funzioni. Ciò ha impedito alcune delle variazioni che ho visto, in cui alcuni algoritmi hanno funzionato meglio con singoli caratteri o stringhe più piccole. Tuttavia, i primi 3 metodi hanno funzionato bene indipendentemente dalla dimensione della stringa.

Set di prova a forcella

http://jsfiddle.net/schmide/fCqp3/134/

// repeated string
var string = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
// count paremeter is changed on every test iteration, limit it's maximum value here
var maxCount = 200;

var n = 0;
$.each(tests, function (name) {
    var fn = tests[name];
    JSLitmus.test(++n + '. ' + name, function (count) {
        var index = 0;
        while (count--) {
            fn.call(string.slice(0, index % string.length), index % maxCount);
            index++;
        }
    });
    if (fn.call('>', 10).length !== 10) $('body').prepend('<h1>Error in "' + name + '"</h1>');
});

JSLitmus.runAll();

Ho quindi incluso la correzione di Dennis e ho deciso di vedere se potevo trovare un modo per uscire un po 'di più.

Dal momento che JavaScript non può davvero ottimizzare le cose, il modo migliore per migliorare le prestazioni è evitare manualmente le cose. Se togliessi i primi 4 risultati insignificanti dal loop, potrei evitare 2-4 string store e scrivere il store finale direttamente sul risultato.

// final: growing pattern + prototypejs check (count < 1)
'final avoid': function (count) {
    if (!count) return '';
    if (count == 1) return this.valueOf();
    var pattern = this.valueOf();
    if (count == 2) return pattern + pattern;
    if (count == 3) return pattern + pattern + pattern;
    var result;
    if (count & 1) result = pattern;
    else result = '';
    count >>= 1;
    do {
        pattern += pattern;
        if (count & 1) result += pattern;
        count >>= 1;
    } while (count > 1);
    return result + pattern + pattern;
}

Ciò ha comportato un miglioramento dell'1-2% in media rispetto alla correzione di Dennis. Tuttavia, diverse esecuzioni e diversi browser mostrerebbero una varianza abbastanza equa che questo codice extra probabilmente non vale la pena rispetto ai 2 algoritmi precedenti.

Un grafico

Modifica: l'ho fatto principalmente con Chrome. Firefox e IE favoriranno spesso Dennis di un paio di%.


0

Metodo semplice:

String.prototype.repeat = function(num) {
    num = parseInt(num);
    if (num < 0) return '';
    return new Array(num + 1).join(this);
}

0

Le persone complicano eccessivamente questo aspetto in misura ridicola o sprecano le prestazioni. Array? Ricorsione? Mi stai prendendo in giro.

function repeat (string, times) {
  var result = ''
  while (times-- > 0) result += string
  return result
}

Modificare. Ho eseguito alcuni semplici test per confrontarli con la versione bit per bit pubblicata da artistoex / disfated e un gruppo di altre persone. Quest'ultimo era solo leggermente più veloce, ma ordini di grandezza più efficienti in termini di memoria. Per 1000000 ripetizioni della parola "blah", il processo Node è salito a 46 megabyte con il semplice algoritmo di concatenazione (sopra), ma solo 5,5 megabyte con l'algoritmo logaritmico. Quest'ultimo è sicuramente la strada da percorrere. Ripubblicarlo per motivi di chiarezza:

function repeat (string, times) {
  var result = ''
  while (times > 0) {
    if (times & 1) result += string
    times >>= 1
    string += string
  }
  return result
}

Hai una string += stringmetà ridondante del tempo.
nikolay,

0

Concatenare stringhe in base a un numero.

function concatStr(str, num) {
   var arr = [];

   //Construct an array
   for (var i = 0; i < num; i++)
      arr[i] = str;

   //Join all elements
   str = arr.join('');

   return str;
}

console.log(concatStr("abc", 3));

Spero che aiuti!


0

Con ES8 puoi anche usare padStarto padEndper questo. per esempio.

var str = 'cat';
var num = 23;
var size = str.length * num;
"".padStart(size, str) // outputs: 'catcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcat'
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.