Répéter la chaîne - Javascript

271

Quelle est la méthode la meilleure ou la plus concise pour retourner une chaîne répétée un nombre arbitraire de fois?

Ce qui suit est mon meilleur coup jusqu'à présent:

function repeat(s, n){
    var a = [];
    while(a.length < n){
        a.push(s);
    }
    return a.join('');
}
Brad
la source
5
Il y a plus de 10 ans, il y avait une solution bien connue à ce problème et que j'ai utilisée comme exemple dans un article sur l'optimisation JavaScript quelques mois avant que vous posiez cette question: webreference.com/programming/javascript/jkm3/3 .html Apparemment, la plupart des gens ont oublié ce code, et je ne vois aucune solution aussi bonne en dessous de la mienne. Le meilleur algorithme semble avoir été retiré de mon code; sauf en raison d'une mauvaise compréhension du fonctionnement de mon code, il effectue une étape supplémentaire de concaténation exponentielle qui est éliminée dans mon original avec une boucle spéciale.
Joseph Myers
10
Personne n'a levé la solution de Joseph. L'algorithme a 3700 ans. Le coût de l'étape supplémentaire est négligeable. Et cet article contient des erreurs et des idées fausses concernant la concaténation de chaînes en Javascript. Pour toute personne intéressée par la façon dont Javascript gère réellement les chaînes en interne, voir Corde .
artistoex
4
Personne ne semble avoir remarqué que la répétition de protoype de chaîne est définie et implémentée, au moins dans Firefox.
kennebec
3
@kennebec: Oui, c'est une fonctionnalité EcmaScript 6 qui n'existait pas lorsque cette question a été posée. C'est assez bien pris en charge maintenant.
rvighne
3
@rvighne - Je viens de vérifier kangax.github.io/compat-table/es6/#String.prototype.repeat Je ne considérerais pas le support exclusivement de Firefox et Chrome comme "assez bien supporté"
aaaaaa

Réponses:

406

Note aux nouveaux lecteurs: cette réponse est ancienne et pas terriblement pratique - elle est juste "intelligente" car elle utilise des trucs Array pour faire les choses String. Quand j'ai écrit "moins de processus", je voulais vraiment dire "moins de code" car, comme d'autres l'ont noté dans les réponses suivantes, il fonctionne comme un cochon. Alors ne l'utilisez pas si la vitesse vous tient à cœur.

Je mettrais cette fonction directement sur l'objet String. Au lieu de créer un tableau, de le remplir et de le joindre avec un caractère vide, créez simplement un tableau de la bonne longueur et joignez-le à la chaîne souhaitée. Même résultat, moins de processus!

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

alert( "string to repeat\n".repeat( 4 ) );
Peter Bailey
la source
36
J'essaie de ne pas étendre les objets natifs, mais sinon c'est une belle solution. Merci!
Brad
34
@ brad - pourquoi pas? Vous préférez polluer l'espace de noms global avec une fonction qui a un home assez bien défini (l'objet String)?
Peter Bailey
16
En fait, vos deux arguments s'appliquent également à l'espace de noms global. Si je veux agrandir un espace de noms et avoir des collisions potentielles, je préfère le faire 1) pas dans le monde 2) dans celui qui est pertinent et 3) est facile à refactoriser. Cela signifie qu'il faut le mettre sur le prototype String, et non pas global.
Peter Bailey
11
une modification que j'apporterais à cette fonction serait de mettre parseInt () autour de "num", car si vous avez une chaîne numérique, vous pourriez obtenir un comportement étrange en raison du jonglage de type JS. par exemple: "ma chaîne" .repeat ("6") == "61"
nickf
19
Si vous ne souhaitez pas étendre les objets natifs, vous pouvez mettre la fonction sur l'objet String au lieu, comme ceci: String.repeat = function(string, num){ return new Array(parseInt(num) + 1).join(string); };. Appelez ça comme ça:String.repeat('/\', 20)
Znarkus
204

J'ai testé les performances de toutes les approches proposées.

Voici la variante la plus rapide que j'ai.

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

Ou en tant que fonction autonome :

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

Il est basé sur l' algorithme artistoex . C'est vraiment rapide. Et countplus elle est grande , plus elle va vite par rapport à l' new Array(count + 1).join(string)approche traditionnelle .

Je n'ai changé que 2 choses:

  1. remplacé pattern = thispar pattern = this.valueOf()(efface une conversion de type évidente);
  2. ajout d'une if (count < 1)vérification de prototypejs en haut de la fonction pour exclure les actions inutiles dans ce cas.
  3. optimisation appliquée de la réponse de Dennis (accélération de 5-7%)

UPD

Créé un petit terrain de jeu pour tester les performances ici pour ceux qui le souhaitent.

variable count~ 0 .. 100:

Diagramme de performance

constante count= 1024:

Diagramme de performance

Utilisez-le et rendez-le encore plus rapide si vous le pouvez :)

déçu
la source
4
Bon travail! Je pense que le count < 1cas est vraiment une optimisation inutile.
JayVee
Excellent algorithme O (log N). Merci pour la grande optimisation avec valueOf ()
vp_arth
2
Les liens d'image sont morts.
Benjamin Gruenbaum
Les liens vont bien. Peut être une indisponibilité temporaire
annulée
Le test JSFiddle ne fonctionne plus correctement; il semble simplement continuer d'exécuter la première fonction maintes et maintes fois (la laisser tourner pendant une demi-heure pour être sûr)
RevanProdigalKnight
47

Ce problème est un problème d'optimisation bien connu / "classique" pour JavaScript, dû au fait que les chaînes JavaScript sont "immuables" et l'ajout par concaténation d'un seul caractère à une chaîne nécessite la création de, y compris l'allocation de mémoire et la copie vers , une toute nouvelle chaîne.

Malheureusement, la réponse acceptée sur cette page est fausse, où "faux" signifie par un facteur de performance de 3x pour les chaînes de caractères simples et de 8x-97x pour les chaînes courtes répétées plusieurs fois, à 300x pour les phrases répétitives, et infiniment incorrect lorsque prendre la limite des rapports de complexité des algorithmes comme nva à l'infini. En outre, il y a une autre réponse sur cette page qui est presque exacte (basée sur l'une des nombreuses générations et variations de la bonne solution qui a circulé sur Internet au cours des 13 dernières années). Cependant, cette solution "presque juste" manque un point clé de l'algorithme correct provoquant une dégradation des performances de 50%.

Résultats de performance JS pour la réponse acceptée, l'autre réponse la plus performante (basée sur une version dégradée de l'algorithme d'origine dans cette réponse), et cette réponse en utilisant mon algorithme créé il y a 13 ans

~ Octobre 2000 J'ai publié un algorithme pour ce problème exact qui a été largement adapté, modifié, puis finalement mal compris et oublié. Pour remédier à ce problème, en août 2008, j'ai publié un article http://www.webreference.com/programming/javascript/jkm3/3.html expliquant l'algorithme et l'utilisant comme exemple de simples optimisations JavaScript à usage général. À ce jour, Web Reference a effacé mes coordonnées et même mon nom dans cet article. Et encore une fois, l'algorithme a été largement adapté, modifié, puis mal compris et largement oublié.

Algorithme JavaScript original de répétition / multiplication de chaînes par Joseph Myers, vers Y2K comme fonction de multiplication de texte dans Text.js; publié en août 2008 sous cette forme par Web Reference: http://www.webreference.com/programming/javascript/jkm3/3.html (L'article a utilisé la fonction comme exemple d'optimisations JavaScript, qui est le seul pour l'étrange nom "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;
}

Dans les deux mois suivant la publication de cet article, cette même question a été publiée sur Stack Overflow et a volé sous mon radar jusqu'à présent, alors qu'apparemment l'algorithme d'origine pour ce problème a de nouveau été oublié. La meilleure solution disponible sur cette page Stack Overflow est une version modifiée de ma solution, éventuellement séparée par plusieurs générations. Malheureusement, les modifications ont ruiné l'optimalité de la solution. En fait, en changeant la structure de la boucle par rapport à mon original, la solution modifiée effectue une étape supplémentaire complètement inutile de la duplication exponentielle (joignant ainsi la plus grande chaîne utilisée dans la bonne réponse avec elle-même un temps supplémentaire, puis la rejetant).

Ci-dessous s'ensuit une discussion sur certaines optimisations JavaScript liées à toutes les réponses à ce problème et pour le bénéfice de tous.

Technique: éviter les références aux objets ou aux propriétés des objets

Pour illustrer le fonctionnement de cette technique, nous utilisons une fonction JavaScript réelle qui crée des chaînes de la longueur nécessaire. Et comme nous le verrons, plus d'optimisations peuvent être ajoutées!

Une fonction comme celle utilisée ici consiste à créer un remplissage pour aligner des colonnes de texte, pour formater de l'argent ou pour remplir des données de bloc jusqu'à la limite. Une fonction de génération de texte permet également une entrée de longueur variable pour tester toute autre fonction qui fonctionne sur du texte. Cette fonction est l'un des composants importants du module de traitement de texte JavaScript.

Au fur et à mesure, nous couvrirons deux autres techniques d'optimisation les plus importantes tout en développant le code original en un algorithme optimisé pour créer des chaînes. Le résultat final est une fonction de haute performance de niveau industriel que j'ai utilisée partout - alignant les prix et les totaux des articles dans les formulaires de commande JavaScript, le formatage des données et le formatage des e-mails / SMS et de nombreuses autres utilisations.

Code original pour créer des chaînes stringFill1()

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

La syntaxe est ici est claire. Comme vous pouvez le voir, nous avons déjà utilisé des variables de fonction locales, avant de passer à d'autres optimisations.

Sachez qu'il existe une référence innocente à une propriété d'objet s.lengthdans le code qui nuit à ses performances. Pire encore, l'utilisation de cette propriété d'objet réduit la simplicité du programme en faisant l'hypothèse que le lecteur connaît les propriétés des objets chaîne JavaScript.

L'utilisation de cette propriété d'objet détruit la généralité du programme informatique. Le programme suppose qu'il xdoit s'agir d'une chaîne de longueur un. Cela limite l'application de la stringFill1()fonction à tout sauf à la répétition de caractères uniques. Même des caractères uniques ne peuvent pas être utilisés s'ils contiennent plusieurs octets comme l'entité HTML &nbsp;.

Le pire problème causé par cette utilisation inutile d'une propriété d'objet est que la fonction crée une boucle infinie si elle est testée sur une chaîne d'entrée vide x. Pour vérifier la généralité, appliquez un programme à la plus petite quantité possible d'entrée. Un programme qui se bloque lorsqu'on lui demande de dépasser la quantité de mémoire disponible a une excuse. Un programme comme celui-ci qui se bloque lorsqu'on lui demande de ne rien produire est inacceptable. Parfois, un joli code est un code toxique.

La simplicité peut être un objectif ambigu de la programmation informatique, mais ce n'est généralement pas le cas. Lorsqu'un programme n'a pas un niveau de généralité raisonnable, il n'est pas valable de dire: "Le programme est assez bon pour autant qu'il va". Comme vous pouvez le voir, l'utilisation de la string.lengthpropriété empêche ce programme de fonctionner dans un paramètre général et, en fait, le programme incorrect est prêt à provoquer un blocage du navigateur ou du système.

Existe-t-il un moyen d'améliorer les performances de ce JavaScript et de résoudre ces deux problèmes graves?

Bien sûr. Utilisez simplement des entiers.

Code optimisé pour créer des chaînes stringFill2()

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

Code temporel à comparer stringFill1()etstringFill2()

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

Le succès jusqu'ici de stringFill2()

stringFill1()prend 47,297 microsecondes (millionièmes de seconde) pour remplir une chaîne de 100 octets et stringFill2()27,68 microsecondes pour faire la même chose. Cela représente presque un doublement des performances en évitant une référence à une propriété d'objet.

Technique: évitez d'ajouter des cordes courtes à des cordes longues

Notre résultat précédent semblait bon - très bon, en fait. La fonction améliorée stringFill2()est beaucoup plus rapide grâce à l'utilisation de nos deux premières optimisations. Le croiriez-vous si je vous disais qu'il peut être amélioré pour être beaucoup plus rapide qu'il ne l'est actuellement?

Oui, nous pouvons atteindre cet objectif. À l'heure actuelle, nous devons expliquer comment nous évitons d'ajouter des chaînes courtes à des chaînes longues.

Le comportement à court terme semble assez bon par rapport à notre fonction d'origine. Les informaticiens aiment analyser le "comportement asymptotique" d'une fonction ou d'un algorithme de programme informatique, ce qui signifie étudier son comportement à long terme en le testant avec des entrées plus importantes. Parfois, sans faire d'autres tests, on ne prend jamais conscience des moyens d'améliorer un programme informatique. Pour voir ce qui va se passer, nous allons créer une chaîne de 200 octets.

Le problème qui apparaît avec stringFill2()

En utilisant notre fonction de synchronisation, nous constatons que le temps augmente à 62,54 microsecondes pour une chaîne de 200 octets, contre 27,68 pour une chaîne de 100 octets. Il semble que le temps devrait être doublé pour faire deux fois plus de travail, mais il est plutôt triplé ou quadruplé. D'après l'expérience de programmation, ce résultat semble étrange, car la fonction devrait être légèrement plus rapide car le travail est effectué plus efficacement (200 octets par appel de fonction plutôt que 100 octets par appel de fonction). Ce problème est lié à une propriété insidieuse des chaînes JavaScript: les chaînes JavaScript sont "immuables".

Immuable signifie que vous ne pouvez pas modifier une chaîne une fois qu'elle a été créée. En ajoutant un octet à la fois, nous n'utilisons pas un octet supplémentaire d'effort. Nous recréons en fait la chaîne entière plus un octet de plus.

En effet, pour ajouter un octet de plus à une chaîne de 100 octets, il faut 101 octets de travail. Analysons brièvement le coût de calcul pour créer une chaîne d' Noctets. Le coût de l'ajout du premier octet est de 1 unité d'effort de calcul. Le coût de l'ajout du deuxième octet n'est pas d'une unité mais de 2 unités (copie du premier octet dans un nouvel objet chaîne ainsi que l'ajout du deuxième octet). Le troisième octet nécessite un coût de 3 unités, etc.

C(N) = 1 + 2 + 3 + ... + N = N(N+1)/2 = O(N^2). Le symbole O(N^2)est prononcé Big O de N au carré, et cela signifie que le coût de calcul à long terme est proportionnel au carré de la longueur de la chaîne. Pour créer 100 caractères, il faut 10 000 unités de travail et pour créer 200 caractères, il faut 40 000 unités de travail.

C'est pourquoi il a fallu plus de deux fois plus de temps pour créer 200 caractères que 100 caractères. En fait, cela aurait dû prendre quatre fois plus de temps. Notre expérience de programmation était correcte dans la mesure où le travail est effectué un peu plus efficacement pour les chaînes plus longues, et donc cela n'a pris que trois fois plus de temps. Une fois que la surcharge de l'appel de fonction devient négligeable quant à la longueur d'une chaîne que nous créons, il faudra en fait quatre fois plus de temps pour créer une chaîne deux fois plus longtemps.

(Remarque historique: cette analyse ne s'applique pas nécessairement aux chaînes dans le code source, par exemple html = 'abcd\n' + 'efgh\n' + ... + 'xyz.\n', puisque le compilateur de code source JavaScript peut réunir les chaînes avant de les transformer en un objet chaîne JavaScript. Il y a quelques années à peine, l'implémentation KJS de JavaScript se bloquait ou se bloquait lors du chargement de longues chaînes de code source jointes par des signes plus. Comme le temps de calcul O(N^2)n'était pas difficile de créer des pages Web qui surchargeaient le navigateur Web Konqueror ou Safari, qui utilisait le noyau du moteur JavaScript KJS. J'ai d'abord suis tombé sur ce problème lorsque je développais un langage de balisage et un analyseur de langage de balisage JavaScript, puis j'ai découvert la cause du problème lorsque j'ai écrit mon script pour les inclusions JavaScript.)

De toute évidence, cette dégradation rapide des performances est un énorme problème. Comment pouvons-nous y faire face, étant donné que nous ne pouvons pas changer la façon dont JavaScript traite les chaînes comme des objets immuables? La solution consiste à utiliser un algorithme qui recrée la chaîne le moins de fois possible.

Pour clarifier, notre objectif est d'éviter d'ajouter des chaînes courtes à des chaînes longues, car pour ajouter la chaîne courte, la chaîne longue entière doit également être dupliquée.

Fonctionnement de l'algorithme pour éviter d'ajouter des chaînes courtes à des chaînes longues

Voici un bon moyen de réduire le nombre de fois où de nouveaux objets chaîne sont créés. Concaténer des longueurs de chaîne plus longues ensemble de sorte que plusieurs octets à la fois soient ajoutés à la sortie.

Par exemple, pour créer une chaîne de longueur 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 */

Pour ce faire, il fallait créer une chaîne de longueur 1, créer une chaîne de longueur 2, créer une chaîne de longueur 4, créer une chaîne de longueur 8 et enfin, créer une chaîne de longueur 9. Combien avons-nous économisé?

Ancien coût C(9) = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 = 45.

Nouveau coût C(9) = 1 + 2 + 4 + 8 + 9 = 24.

Notez que nous devions ajouter une chaîne de longueur 1 à une chaîne de longueur 0, puis une chaîne de longueur 1 à une chaîne de longueur 1, puis une chaîne de longueur 2 à une chaîne de longueur 2, puis une chaîne de longueur 4 à une chaîne de longueur 4, puis une chaîne de longueur 8 à une chaîne de longueur 1, afin d'obtenir une chaîne de longueur 9. Ce que nous faisons peut être résumé en évitant d'ajouter des chaînes courtes à des chaînes longues, ou dans d'autres mots, en essayant de concaténer des chaînes de longueur égale ou presque égale.

Pour l'ancien coût de calcul, nous avons trouvé une formule N(N+1)/2. Existe-t-il une formule pour le nouveau coût? Oui, mais c'est compliqué. L'important est que ce soit le cas O(N), et donc doubler la longueur de la chaîne doublera approximativement la quantité de travail plutôt que de la quadrupler.

Le code qui implémente cette nouvelle idée est presque aussi compliqué que la formule du coût de calcul. Lorsque vous le lisez, n'oubliez pas que cela >>= 1signifie de décaler vers la droite d'un octet. Donc, si n = 10011est un nombre binaire, alors n >>= 1résulte en la valeur n = 1001.

L'autre partie du code que vous pourriez ne pas reconnaître est l'écriture au niveau du bit et de l'opérateur &. L'expression n & 1évalue vrai si le dernier chiffre binaire de nest 1 et faux si le dernier chiffre binaire de nest 0.

Nouveau très efficace stringFill3()fonction

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

Il semble laid à l'œil non averti, mais ses performances ne sont rien de moins que charmantes.

Voyons à quel point cette fonction fonctionne bien. Après avoir vu les résultats, il est probable que vous n'oublierez jamais la différence entre un O(N^2)algorithme et un O(N)algorithme.

stringFill1()prend 88,7 microsecondes (millionièmes de seconde) pour créer une chaîne de 200 octets, stringFill2()prend 62,54 et stringFill3()ne prend que 4,608. Qu'est-ce qui a rendu cet algorithme tellement meilleur? Toutes les fonctions ont profité de l'utilisation de variables de fonction locales, mais en profitant des deuxième et troisième techniques d'optimisation, les performances ont été multipliées par vingt stringFill3().

Analyse plus approfondie

Qu'est-ce qui fait que cette fonction particulière fait exploser la concurrence hors de l'eau?

Comme je l'ai mentionné, la raison pour laquelle ces deux fonctions stringFill1()et stringFill2()s'exécutent si lentement est que les chaînes JavaScript sont immuables. La mémoire ne peut pas être réallouée pour permettre d'ajouter un octet de plus à la fois aux données de chaîne stockées par JavaScript. Chaque fois qu'un octet supplémentaire est ajouté à la fin de la chaîne, la chaîne entière est régénérée du début à la fin.

Ainsi, afin d'améliorer les performances du script, il faut précalculer des chaînes de longueur plus longue en concaténant deux chaînes ensemble à l'avance, puis en construisant récursivement la longueur de chaîne souhaitée.

Par exemple, pour créer une chaîne d'octets de 16 lettres, une chaîne de deux octets serait d'abord précalculée. Ensuite, la chaîne de deux octets serait réutilisée pour précalculer une chaîne de quatre octets. Ensuite, la chaîne de quatre octets serait réutilisée pour précalculer une chaîne de huit octets. Enfin, deux chaînes de huit octets seraient réutilisées pour créer la nouvelle chaîne souhaitée de 16 octets. Au total, quatre nouvelles chaînes ont dû être créées, une de longueur 2, une de longueur 4, une de longueur 8 et une de longueur 16. Le coût total est de 2 + 4 + 8 + 16 = 30.

À long terme, cette efficacité peut être calculée en ajoutant dans l'ordre inverse et en utilisant une série géométrique commençant par un premier terme a1 = N et ayant un rapport commun de r = 1/2. La somme d'une série géométrique est donnée par a_1 / (1-r) = 2N.

C'est plus efficace que d'ajouter un caractère pour créer une nouvelle chaîne de longueur 2, créant une nouvelle chaîne de longueur 3, 4, 5, etc. jusqu'à 16. L'algorithme précédent utilisait ce processus d'ajout d'un seul octet à la fois , et le coût total serait n (n + 1) / 2 = 16 (17) / 2 = 8 (17) = 136.

De toute évidence, 136 est un nombre beaucoup plus grand que 30, et donc l'algorithme précédent prend beaucoup, beaucoup plus de temps pour construire une chaîne.

Pour comparer les deux méthodes, vous pouvez voir à quel point l'algorithme récursif (également appelé "diviser pour mieux régner") est plus rapide sur une chaîne de longueur 123 457. Sur mon ordinateur FreeBSD, cet algorithme, implémenté dans la stringFill3()fonction, crée la chaîne en 0,001058 secondes, tandis que la stringFill1()fonction d' origine crée la chaîne en 0,0808 secondes. La nouvelle fonction est 76 fois plus rapide.

La différence de performances augmente à mesure que la longueur de la chaîne augmente. Dans la limite où des chaînes de plus en plus grandes sont créées, la fonction d'origine se comporte à peu près comme des temps C1(constants) N^2et la nouvelle fonction se comporte comme des temps C2(constants) N.

À partir de notre expérience, nous pouvons déterminer la valeur de l' C1être C1 = 0.0808 / (123457)2 = .00000000000530126997et la valeur de l' C2être C2 = 0.001058 / 123457 = .00000000856978543136. En 10 secondes, la nouvelle fonction pourrait créer une chaîne contenant 1 166 890 359 caractères. Pour créer cette même chaîne, l'ancienne fonction aurait besoin de 7 218 384 secondes.

C'est près de trois mois contre dix secondes!

Je ne réponds (plusieurs années en retard) que parce que ma solution originale à ce problème flotte sur Internet depuis plus de 10 ans, et est apparemment encore mal comprise par quelques-uns qui s'en souviennent. Je pensais qu'en écrivant un article à ce sujet ici, j'aiderais:

Optimisations des performances pour JavaScript haute vitesse / Page 3

Malheureusement, certaines des autres solutions présentées ici sont encore certaines de celles qui prendraient trois mois pour produire la même quantité de sortie qu'une solution appropriée crée en 10 secondes.

Je veux prendre le temps de reproduire une partie de l'article ici comme une réponse canonique sur Stack Overflow.

Notez que l'algorithme le plus performant ici est clairement basé sur mon algorithme et a probablement été hérité de l'adaptation de 3e ou 4e génération de quelqu'un d'autre. Malheureusement, les modifications ont entraîné une réduction de ses performances. La variation de ma solution présentée ici n'a peut-être pas compris mon for (;;)expression déroutante qui ressemble à la boucle infinie principale d'un serveur écrit en C, et qui a été simplement conçue pour permettre une instruction de rupture soigneusement positionnée pour le contrôle de la boucle, le moyen le plus compact de éviter de répliquer de façon exponentielle la chaîne une fois de plus inutile.

Joseph Myers
la source
4
Cette réponse n'aurait pas dû recevoir autant de votes positifs. Tout d'abord, les revendications de propriété de Joseph sont ridicules. L'algorithme sous-jacent a 3700 ans.
artistoex
2
Deuxièmement, il contient beaucoup de désinformation. Les implémentations Javascript modernes ne touchent même pas le contenu d'une chaîne lors de l'exécution de la concaténation (la v8 représente les chaînes concaténées comme un objet de type ConsString). Toutes les améliorations restantes sont négligeables (en termes de complexité asymptotique).
artistoex
3
Votre idée de la façon dont les chaînes sont concaténées est fausse. Pour concaténer deux chaînes, Javascript ne lit pas du tout les octets des chaînes constitutives. Au lieu de cela, il crée simplement un objet qui fait référence aux parties gauche et droite. C'est pourquoi la dernière concaténation de la boucle n'est pas plus coûteuse que les premières.
artistoex
3
Bien sûr, cela entraîne un coût supérieur à O (1) pour l'indexation de la chaîne, de sorte que la concaténation peut être aplatie ultérieurement, ce qui mérite en effet une évaluation plus approfondie.
artistoex
1
C'était une excellente lecture. Vous devriez écrire un livre sur l'efficacité et tout ça!
39

Celui-ci est assez efficace

String.prototype.repeat = function(times){
    var result="";
    var pattern=this;
    while (times > 0) {
        if (times&1)
            result+=pattern;
        times>>=1;
        pattern+=pattern;
    }
    return result;
};
artistoex
la source
11
@Olegs, je pense que l'idée de voter est moins que de voter pour une personne ou pour la créativité d'une personne (ce qui est effectivement applaudissable), mais l'idée est de voter pour la solution la plus complète, afin qu'elle puisse être facilement trouvée sur haut de la liste, sans avoir à lire toutes les réponses dans la recherche de la parfaite. (Parce que, malheureusement, nous avons tous un temps limité ...)
Sorin Postelnicu
38

Bonnes nouvelles! String.prototype.repeatfait maintenant partie de JavaScript .

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

La méthode est prise en charge par tous les principaux navigateurs, à l'exception d'Internet Explorer et d'Android Webview. Pour une liste à jour, voir MDN: String.prototype.repeat> Compatibilité du navigateur .

MDN a un polyfill pour les navigateurs sans support.

André Laszlo
la source
Merci de rapporter l'état actuel des choses, bien que je pense que le polyfill Mozilla est bien compliqué pour la plupart des besoins (je suppose qu'ils essaient d'imiter le comportement de leur implémentation C efficace) - donc ne répondra pas vraiment à l'exigence de concision du PO. Toutes les autres approches mises en place en tant que polyfill doivent être plus concises ;-)
Guss
2
Absolument! Mais l'utilisation de la fonction intégrée doit être la version la plus concise. Étant donné que les polyfills ne sont essentiellement que des ports arrière, ils ont tendance à être un peu complexes pour assurer la compatibilité avec les spécifications (ou les spécifications proposées, dans ce cas). Je l'ai ajouté pour être complet, c'est au PO de décider quelle méthode utiliser, je suppose.
André Laszlo
19

String.prototype.repeat est désormais ES6 Standard.

'abc'.repeat(3); //abcabcabc
Lewis
la source
agréable! .. mais pas utilisable pour moi (ce n'est pas pris en charge sur ios <9): kangax.github.io/compat-table/es6
Benjamin
@Benjamin Utilisez le polyfill sur le lien.
Lewis
Je pense que devrait être une réponse épinglée .
test30
17

Extension de la solution de P.Bailey :

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

De cette façon, vous devez être à l'abri des types d'arguments inattendus:

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: Crédits à jerone pour son ++numidée élégante !

antichris
la source
2
Vous avez un peu changé:String.prototype.repeat = function(n){return new Array(isNaN(n) ? 1 : ++n).join(this);}
jerone
Quoi qu'il en soit, selon ce test ( jsperf.com/string-repeat/2 ), faire une boucle simple avec concaténation de chaînes semble être beaucoup plus rapide sur Chrome que d'utiliser Array.join. N'est-ce pas drôle?!
Marco Demaio
8

Utilisation Array(N+1).join("string_to_repeat")

Kalpesh Patel
la source
J'aime ça. Je sais pourquoi ce n'est pas là-haut.
Joe Thomas
aimer. si simple et propre
ekkis
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
};

voici comment répéter la chaîne plusieurs fois à l'aide du délimètre.

BitOfUniverse
la source
4

Voici une amélioration de 5 à 7% de la réponse de Disfated.

Déroulez la boucle en vous arrêtant sur count > 1et effectuez un result += pattnernconcat supplémentaire après la boucle. Cela évitera les boucles finales précédemment inutilisées pattern += patternsans avoir à utiliser un if-check coûteux. Le résultat final ressemblerait à ceci:

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

Et voici le violon de disfated fourchu pour la version déroulée: http://jsfiddle.net/wsdfg/

Dennis
la source
2
function repeat(s, n) { var r=""; for (var a=0;a<n;a++) r+=s; return r;}
Joel Coehoorn
la source
2
La concaténation de chaînes n'est-elle pas coûteuse? C'est du moins le cas en Java.
Vijay Dev
Pourquoi oui ils le sont. Cependant, il ne peut pas vraiment être optimisé en javarscript. :(
McTrafik
Qu'en est-il de cette amélioration des performances: var r=s; for (var a=1;...:)))) Quoi qu'il en soit, selon ce test ( jsperf.com/string-repeat/2 ), faire une boucle simple avec concaténation de chaînes comme ce que vous avez suggéré semble être beaucoup plus rapide sur Chrome que d'utiliser Array .joindre.
Marco Demaio
@VijayDev - pas selon ce test: jsperf.com/ultimate-concat-vs-join
jbyrd
2

Tests des différentes méthodes:

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));
    }
}
Fordi
la source
2

Voici la version sûre de JSLint

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

Pour tous les navigateurs

C'est à peu près aussi concis que possible:

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

Si vous vous souciez également de la performance, c'est une bien meilleure approche:

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

Si vous souhaitez comparer les performances des deux options, consultez ce violon et ce violon pour des tests de référence. Lors de mes propres tests, la deuxième option était environ 2 fois plus rapide dans Firefox et environ 4 fois plus rapide dans Chrome!

Pour les navigateurs modernes uniquement:

Dans les navigateurs modernes, vous pouvez désormais également procéder comme suit:

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

Cette option est non seulement plus courte que les deux autres options, mais elle est encore plus rapide que la deuxième option.

Malheureusement, cela ne fonctionne dans aucune version d'Internet Explorer. Les chiffres du tableau spécifient la première version du navigateur qui prend entièrement en charge la méthode:

entrez la description de l'image ici

John Slegers
la source
2

Juste une autre fonction de répétition:

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

ES2015a été réalisé cette repeat()méthode!

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/ Chaîne / répétition
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

xgqfrms
la source
1

Ce peut être le plus petit récursif: -

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

Concaténation récursive simple

Je voulais juste lui donner un coup bash, et j'ai fait ceci:

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

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

Je ne peux pas dire que j'y ai beaucoup réfléchi, et cela montre probablement :-)

C'est sans doute mieux

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

"foo".ditto( 128 );

Et cela ressemble beaucoup à une réponse déjà publiée - je le sais.

Mais pourquoi être récursif?

Et que diriez-vous d'un petit comportement par défaut aussi?

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

"foo".ditto();

Parce que, bien que la méthode non récursive gère des répétitions arbitrairement grandes sans atteindre les limites de la pile d'appels, elle est beaucoup plus lente.

Pourquoi ai-je pris la peine d'ajouter plus de méthodes qui ne sont pas à moitié aussi intelligentes que celles déjà publiées?

En partie pour mon propre amusement, et en partie pour souligner de la manière la plus simple, je sais qu'il existe de nombreuses façons d'écorcher un chat, et selon la situation, il est fort possible que la meilleure méthode apparemment ne soit pas idéale.

Une méthode relativement rapide et sophistiquée peut effectivement planter et brûler dans certaines circonstances, tandis qu'une méthode plus lente et plus simple peut faire le travail - éventuellement.

Certaines méthodes peuvent être un peu plus que des exploits, et en tant que telles sujettes à être corrigées de l'existence, et d'autres méthodes peuvent fonctionner à merveille dans toutes les conditions, mais sont si construites que l' on n'a tout simplement aucune idée de comment cela fonctionne.

"Et si je ne sais pas comment ça marche?!"

Sérieusement?

JavaScript souffre de l'une de ses plus grandes forces; il est très tolérant aux mauvais comportements, et si flexible qu'il se penchera en arrière pour retourner des résultats, alors qu'il aurait pu être mieux pour tout le monde s'il s'était cassé!

"Avec une grande puissance, vient une grande responsabilité" ;-)

Mais plus sérieusement et plus important encore, bien que des questions générales comme celle-ci conduisent à une impressionnant sous forme de réponses intelligentes qui, si rien d'autre, élargit ses connaissances et ses horizons, en fin de compte, la tâche à accomplir - le script pratique qui utilise la méthode résultante - peut nécessiter un peu moins, ou un peu plus intelligent que ce qui est suggéré.

Ces algorithmes «parfaits» sont amusants et tout, mais «taille unique» sera rarement, sinon jamais, meilleur que sur mesure.

Ce sermon vous a été présenté grâce à un manque de sommeil et à un intérêt passager. Allez-y et codez!

Fred Gandt
la source
1

Premièrement, les questions du PO semblent concerner la concision - que je comprends comme «simple et facile à lire», tandis que la plupart des réponses semblent concerner l'efficacité - ce qui n'est évidemment pas la même chose et je pense aussi que si vous ne des algorithmes spécifiques de manipulation de données volumineuses ne devraient pas vous inquiéter lorsque vous venez d'implémenter des fonctions Javascript de manipulation de données de base. La concision est beaucoup plus importante.

Deuxièmement, comme André Laszlo l'a noté, String.repeat fait partie d'ECMAScript 6 et est déjà disponible dans plusieurs implémentations populaires - donc l'implémentation la plus concise de String.repeatne pas l'implémenter ;-)

Enfin, si vous devez prendre en charge des hôtes qui n'offrent pas l'implémentation d'ECMAScript 6, le polyfill MDN mentionné par André Laszlo est tout sauf concis.

Donc, sans plus tarder - voici mon polyfill concis :

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

Oui, c'est une récursivité. J'aime les récursions - elles sont simples et si elles sont faites correctement, elles sont faciles à comprendre. En ce qui concerne l'efficacité, si la langue le prend en charge, elle peut être très efficace si elle est écrite correctement.

D'après mes tests, cette méthode est ~ 60% plus rapide que l' Array.joinapproche. Bien que cela ne soit manifestement pas du tout conforme à la mise en œuvre de Disfated, il est beaucoup plus simple que les deux.

Ma configuration de test est le nœud v0.10, en utilisant le "mode strict" (je pense qu'il permet une sorte de TCO ), en appelant repeat(1000)une chaîne de 10 caractères un million de fois.

Guss
la source
1

Si vous pensez que toutes ces définitions de prototypes, créations de tableaux et opérations de jointure sont excessives, utilisez simplement un code de ligne unique là où vous en avez besoin. La chaîne S se répète N fois:

for (var i = 0, result = ''; i < N; i++) result += S;
Semra
la source
3
Le code doit être lisible. Si vous ne l'utilisez qu'une seule fois, formatez-le correctement (ou utilisez la Array(N + 1).join(str)méthode si ce n'est pas un goulot d'étranglement). S'il y a la moindre chance que vous l'utilisiez deux fois, déplacez-le vers une fonction correctement nommée.
cloudfeet
1

Utilisez Lodash pour la fonctionnalité de l'utilitaire Javascript, comme la répétition de chaînes.

Lodash offre de belles performances et une compatibilité ECMAScript.

Je le recommande vivement pour le développement de l'interface utilisateur et cela fonctionne bien côté serveur également.

Voici comment répéter la chaîne "yo" 2 fois en utilisant Lodash:

> _.repeat('yo', 2)
"yoyo"
l3x
la source
0

Solution récursive utilisant diviser pour régner:

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); }
}
Fordi
la source
0

Je suis venu ici au hasard et je n'ai jamais eu de raison de répéter un caractère en javascript auparavant.

J'ai été impressionné par la façon de faire de artistoex et par les résultats déçus. J'ai remarqué que le dernier enchaînement de chaînes n'était pas nécessaire, comme Dennis l'a également souligné.

J'ai remarqué un peu plus de choses en jouant avec l'échantillonnage déformé.

Les résultats variaient assez souvent, favorisant souvent la dernière manche et des algorithmes similaires se disputaient souvent la position. L'une des choses que j'ai changé était au lieu d'utiliser le compte généré par JSLitmus comme la graine pour les appels; comme le nombre a été généré différemment pour les différentes méthodes, j'ai mis un index. Cela a rendu la chose beaucoup plus fiable. J'ai ensuite vérifié si des chaînes de tailles différentes étaient transmises aux fonctions. Cela a empêché certaines des variations que j'ai vues, où certains algorithmes ont fait mieux au niveau des caractères uniques ou des chaînes plus petites. Cependant, les 3 meilleures méthodes ont toutes bien fonctionné quelle que soit la taille de la chaîne.

Kit de test fourchu

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

J'ai ensuite inclus le correctif de Dennis et décidé de voir si je pouvais trouver un moyen de sortir un peu plus.

Étant donné que javascript ne peut pas vraiment optimiser les choses, la meilleure façon d'améliorer les performances est d'éviter manuellement les choses. Si je retirais les 4 premiers résultats triviaux de la boucle, je pouvais éviter 2 à 4 magasins de chaînes et écrire le magasin final directement dans le résultat.

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

Cela a entraîné une amélioration de 1 à 2% en moyenne par rapport au correctif de Dennis. Cependant, différentes exécutions et différents navigateurs montreraient une variance assez juste pour que ce code supplémentaire ne vaille probablement pas l'effort par rapport aux 2 algorithmes précédents.

Un graphique

Edit: je l'ai fait principalement sous chrome. Firefox et IE favoriseront souvent Dennis d'un couple%.

Andrew Hallendorff
la source
0

Méthode simple:

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

Les gens compliquent cela de façon ridicule ou gâchent les performances. Des tableaux? Récursivité? Dis moi que c'est une blague.

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

Éditer. J'ai effectué des tests simples pour comparer avec la version au niveau du bit publiée par artistoex / disfated et un tas d'autres personnes. Ce dernier n'était que légèrement plus rapide, mais les ordres de grandeur étaient plus efficaces en mémoire. Pour 1000000 répétitions du mot «bla», le processus Node est passé à 46 mégaoctets avec l'algorithme de concaténation simple (ci-dessus), mais seulement 5,5 mégaoctets avec l'algorithme logarithmique. Ce dernier est certainement la voie à suivre. Le republier pour plus de clarté:

function repeat (string, times) {
  var result = ''
  while (times > 0) {
    if (times & 1) result += string
    times >>= 1
    string += string
  }
  return result
}
Nelo Mitranim
la source
Vous avez une string += stringmoitié du temps redondante .
nikolay
0

Concaténation de chaînes basées sur un nombre.

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

J'espère que cela pourra aider!

loxsat
la source
0

Avec ES8, vous pouvez également utiliser padStartou padEndpour cela. par exemple.

var str = 'cat';
var num = 23;
var size = str.length * num;
"".padStart(size, str) // outputs: 'catcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcat'
wizzfizz94
la source