Repetir cadena - Javascript

271

¿Cuál es el método mejor o más conciso para devolver una cadena repetida una cantidad arbitraria de veces?

El siguiente es mi mejor tiro hasta ahora:

function repeat(s, n){
    var a = [];
    while(a.length < n){
        a.push(s);
    }
    return a.join('');
}
puntilla
fuente
55
Hace más de 10 años, había una solución mía bien conocida para este problema, que utilicé como ejemplo en un artículo de optimización de JavaScript un par de meses antes de que hiciera esta pregunta: webreference.com/programming/javascript/jkm3/3 .html Aparentemente, la mayoría de la gente se ha olvidado de ese código, y no veo ninguna solución tan buena como la mía. Parece que el mejor algoritmo fue sacado de mi código; excepto debido a un malentendido de cómo funciona mi código, realiza un paso adicional de concatenación exponencial que se elimina en mi original con un bucle especial.
Joseph Myers
10
Nadie levantó la solución de Joseph. El algoritmo tiene 3700 años. El costo del paso adicional es insignificante. Y este artículo contiene errores y conceptos erróneos con respecto a la concatenación de cadenas en Javascript. Para cualquier persona interesada en cómo Javascript realmente maneja las cadenas internamente, vea Rope .
artistoex
44
Nadie parece haberse dado cuenta de que la repetición del prototipo de cadena está definida e implementada, al menos en Firefox.
Kennebec
3
@kennebec: Sí, esa es una característica de EcmaScript 6 que no existía cuando se hizo esta pregunta. Está bastante bien soportado ahora.
rvighne
3
@rvighne - Acabo de comprobar kangax.github.io/compat-table/es6/#String.prototype.repeat . No consideraría el soporte exclusivo de Firefox y Chrome como "bastante bien soportado"
aaaaaa

Respuestas:

406

Nota para los nuevos lectores: esta respuesta es antigua y no es terriblemente práctica: es simplemente "inteligente" porque usa cosas de Array para hacer las cosas de String. Cuando escribí "menos proceso" definitivamente quise decir "menos código" porque, como otros han señalado en respuestas posteriores, funciona como un cerdo. Así que no lo uses si la velocidad te importa.

Pondría esta función directamente en el objeto String. En lugar de crear una matriz, llenarla y unirla con un carácter vacío, simplemente cree una matriz de la longitud adecuada y únala con la cadena deseada. ¡El mismo resultado, menos proceso!

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

alert( "string to repeat\n".repeat( 4 ) );
Peter Bailey
fuente
36
Intento no extender objetos nativos, pero de lo contrario, esta es una solución hermosa. ¡Gracias!
Brad
34
@ Brad: ¿por qué no? ¿Prefieres contaminar el espacio de nombres global con una función que tenga un inicio bastante bien definido (el objeto String)?
Peter Bailey
16
En realidad, ambos argumentos se aplican también al espacio de nombres global. Si voy a expandir un espacio de nombres y tengo posibles colisiones, prefiero hacerlo 1) no en global 2) en uno que sea relevante y 3) es fácil de refactorizar. Esto significa ponerlo en el prototipo de String, no en global.
Peter Bailey
11
un cambio que haría a esta función sería poner parseInt () alrededor de "num", ya que si tiene una cadena numérica, puede obtener un comportamiento extraño debido al malabarismo de tipo JS. por ejemplo: "my string" .repeat ("6") == "61"
nickf
19
Si no desea extender objetos nativos, se puede poner la función en el objeto String en lugar, de esta manera: String.repeat = function(string, num){ return new Array(parseInt(num) + 1).join(string); };. Llámalo así:String.repeat('/\', 20)
Znarkus
204

He probado el rendimiento de todos los enfoques propuestos.

Aquí está la variante más rápida que tengo.

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 como función independiente :

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

Se basa en el algoritmo artistoex . Es realmente rapido. Y cuanto más grande count, más rápido va en comparación con el new Array(count + 1).join(string)enfoque tradicional .

Solo he cambiado 2 cosas:

  1. reemplazado pattern = thiscon pattern = this.valueOf()(borra una conversión de tipo obvia);
  2. Se agregó un if (count < 1)control de prototypejs a la parte superior de la función para excluir acciones innecesarias en ese caso.
  3. optimización aplicada de la respuesta de Dennis (5-7% de velocidad)

UPD

Creé un pequeño patio de pruebas de rendimiento aquí para aquellos interesados.

variable count~ 0 .. 100:

Diagrama de rendimiento

constante count= 1024:

Diagrama de rendimiento

Úselo y hágalo aún más rápido si puede :)

desvanecido
fuente
44
¡Buen trabajo! Creo que el count < 1caso es una optimización realmente innecesaria.
JayVee
Excelente algoritmo O (log N). Gracias por una gran optimización con valueOf ()
vp_arth
2
Los enlaces de imágenes están muertos.
Benjamin Gruenbaum
Los enlaces están bien. Puede ser indisponibilidad temporal
desapareció
La prueba JSFiddle ya no funciona correctamente; parece que acaba de seguir corriendo la primera función una y otra vez (lo dejó correr durante media hora para estar seguro)
RevanProdigalKnight
47

Este problema es un problema de optimización bien conocido / "clásico" para JavaScript, causado por el hecho de que las cadenas de JavaScript son "inmutables" y la adición por concatenación de incluso un solo carácter a una cadena requiere la creación de, incluida la asignación de memoria y la copia a , una cadena completamente nueva.

Desafortunadamente, la respuesta aceptada en esta página es incorrecta, donde "incorrecto" significa un factor de rendimiento de 3x para cadenas simples de un carácter, y 8x-97x para cadenas cortas repetidas más veces, a 300x para oraciones repetidas, e infinitamente incorrecto cuando tomando el límite de las razones de complejidad de los algoritmos nhasta el infinito. Además, hay otra respuesta en esta página que es casi correcta (basada en una de las muchas generaciones y variaciones de la solución correcta que ha circulado por Internet en los últimos 13 años). Sin embargo, esta solución "casi correcta" pierde un punto clave del algoritmo correcto y causa una degradación del rendimiento del 50%.

JS Performance Results para la respuesta aceptada, la otra respuesta de mejor desempeño (basada en una versión degradada del algoritmo original en esta respuesta), y esta respuesta usando mi algoritmo creado hace 13 años

~ Octubre de 2000 Publiqué un algoritmo para este problema exacto que fue ampliamente adaptado, modificado, y finalmente mal entendido y olvidado. Para solucionar este problema, en agosto de 2008 publiqué un artículo en http://www.webreference.com/programming/javascript/jkm3/3.html que explica el algoritmo y lo utiliza como un ejemplo de optimizaciones simples de JavaScript de uso general. Por ahora, Web Reference ha borrado mi información de contacto e incluso mi nombre de este artículo. Y una vez más, el algoritmo ha sido ampliamente adaptado, modificado, luego mal entendido y en gran parte olvidado.

Algoritmo JavaScript original de repetición / multiplicación de cadenas por Joseph Myers, alrededor de Y2K como una función de multiplicación de texto dentro de Text.js; publicado en agosto de 2008 en este formulario por Web Reference: http://www.webreference.com/programming/javascript/jkm3/3.html (El artículo utilizó la función como ejemplo de optimizaciones de JavaScript, que es la única para los extraños nombre "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;
}

Dos meses después de la publicación de ese artículo, esta misma pregunta fue publicada en Stack Overflow y pasó desapercibida hasta ahora, cuando aparentemente el algoritmo original para este problema ha sido olvidado nuevamente. La mejor solución disponible en esta página de desbordamiento de pila es una versión modificada de mi solución, posiblemente separada por varias generaciones. Desafortunadamente, las modificaciones arruinaron la optimización de la solución. De hecho, al cambiar la estructura del bucle de mi original, la solución modificada realiza un paso adicional completamente innecesario de duplicación exponencial (uniendo así la cadena más grande utilizada en la respuesta adecuada consigo mismo un tiempo extra y luego descartándola).

A continuación se presenta una discusión sobre algunas optimizaciones de JavaScript relacionadas con todas las respuestas a este problema y en beneficio de todos.

Técnica: evitar referencias a objetos o propiedades de objetos

Para ilustrar cómo funciona esta técnica, utilizamos una función de JavaScript de la vida real que crea cadenas de cualquier longitud necesaria. Y como veremos, ¡se pueden agregar más optimizaciones!

Una función como la que se usa aquí es crear relleno para alinear columnas de texto, formatear dinero o rellenar datos de bloque hasta el límite. Una función de generación de texto también permite la entrada de longitud variable para probar cualquier otra función que opera en texto. Esta función es uno de los componentes importantes del módulo de procesamiento de texto JavaScript.

A medida que avanzamos, cubriremos dos más de las técnicas de optimización más importantes mientras desarrollamos el código original en un algoritmo optimizado para crear cadenas. El resultado final es una función industrial de alto rendimiento que he usado en todas partes, alineando los precios de los artículos y los totales en los formularios de pedido de JavaScript, el formato de datos y el formato de correo electrónico / mensaje de texto y muchos otros usos.

Código original para crear cadenas stringFill1()

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

La sintaxis está aquí es clara. Como puede ver, ya hemos usado variables de función local, antes de continuar con más optimizaciones.

Tenga en cuenta que hay una referencia inocente a una propiedad de objeto s.lengthen el código que perjudica su rendimiento. Peor aún, el uso de esta propiedad de objeto reduce la simplicidad del programa al suponer que el lector conoce las propiedades de los objetos de cadena de JavaScript.

El uso de esta propiedad de objeto destruye la generalidad del programa de computadora. El programa supone que xdebe ser una cadena de longitud uno. Esto limita la aplicación de la stringFill1()función a cualquier cosa, excepto la repetición de caracteres individuales. Incluso los caracteres individuales no se pueden usar si contienen múltiples bytes como la entidad HTML &nbsp;.

El peor problema causado por este uso innecesario de una propiedad de objeto es que la función crea un bucle infinito si se prueba en una cadena de entrada vacía x. Para verificar la generalidad, aplique un programa a la menor cantidad de entrada posible. Un programa que falla cuando se le pide que exceda la cantidad de memoria disponible tiene una excusa. Un programa como este que se bloquea cuando se le pide que no produzca nada es inaceptable. A veces, el código bonito es un código venenoso.

La simplicidad puede ser un objetivo ambiguo de la programación de computadoras, pero generalmente no lo es. Cuando un programa carece de un nivel razonable de generalidad, no es válido decir: "El programa es lo suficientemente bueno hasta donde llega". Como puede ver, el uso de la string.lengthpropiedad evita que este programa funcione en una configuración general y, de hecho, el programa incorrecto está listo para provocar un bloqueo del navegador o del sistema.

¿Hay alguna manera de mejorar el rendimiento de este JavaScript y de solucionar estos dos problemas serios?

Por supuesto. Solo usa números enteros.

Código optimizado para crear cadenas stringFill2()

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

Código de tiempo para comparar stringFill1()ystringFill2()

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

El éxito hasta ahora de stringFill2()

stringFill1()toma 47.297 microsegundos (millonésimas de segundo) para llenar una cadena de 100 bytes, y stringFill2()toma 27.68 microsegundos para hacer lo mismo. Eso es casi una duplicación en el rendimiento al evitar una referencia a una propiedad de objeto.

Técnica: evite agregar cadenas cortas a cadenas largas

Nuestro resultado anterior se veía bien, muy bueno, de hecho. La función mejorada stringFill2()es mucho más rápida debido al uso de nuestras dos primeras optimizaciones. ¿Lo creerías si te dijera que se puede mejorar para que sea mucho más rápido de lo que es ahora?

Sí, podemos lograr ese objetivo. En este momento necesitamos explicar cómo evitamos agregar cadenas cortas a cadenas largas.

El comportamiento a corto plazo parece ser bastante bueno, en comparación con nuestra función original. A los científicos informáticos les gusta analizar el "comportamiento asintótico" de una función o algoritmo de programa informático, lo que significa estudiar su comportamiento a largo plazo probándolo con entradas más grandes. A veces, sin realizar más pruebas, uno nunca se da cuenta de las formas en que un programa de computadora podría mejorarse. Para ver qué sucederá, crearemos una cadena de 200 bytes.

El problema que aparece con stringFill2()

Usando nuestra función de temporización, encontramos que el tiempo aumenta a 62.54 microsegundos para una cadena de 200 bytes, en comparación con 27.68 para una cadena de 100 bytes. Parece que el tiempo debería duplicarse para hacer el doble de trabajo, pero en su lugar se triplicó o cuadruplicó. Según la experiencia de programación, este resultado parece extraño, porque en todo caso, la función debería ser un poco más rápida ya que el trabajo se realiza de manera más eficiente (200 bytes por llamada a la función en lugar de 100 bytes por llamada a la función). Este problema tiene que ver con una propiedad insidiosa de las cadenas de JavaScript: las cadenas de JavaScript son "inmutables".

Inmutable significa que no puede cambiar una cadena una vez que se ha creado. Al agregar un byte a la vez, no estamos usando un byte más de esfuerzo. En realidad, estamos recreando la cadena completa más un byte más.

En efecto, para agregar un byte más a una cadena de 100 bytes, se requieren 101 bytes de trabajo. Analicemos brevemente el costo computacional para crear una cadena de Nbytes. El costo de agregar el primer byte es 1 unidad de esfuerzo computacional. El costo de agregar el segundo byte no es una unidad sino 2 unidades (copiar el primer byte a un nuevo objeto de cadena y agregar el segundo byte). El tercer byte requiere un costo de 3 unidades, etc.

C(N) = 1 + 2 + 3 + ... + N = N(N+1)/2 = O(N^2). El símbolo O(N^2)se pronuncia Big O de N al cuadrado, y significa que el costo computacional a largo plazo es proporcional al cuadrado de la longitud de la cadena. Crear 100 caracteres requiere 10,000 unidades de trabajo, y crear 200 caracteres requiere 40,000 unidades de trabajo.

Es por eso que tomó más del doble de tiempo crear 200 caracteres que 100 caracteres. De hecho, debería haber tardado cuatro veces más. Nuestra experiencia en programación fue correcta en el sentido de que el trabajo se realiza de manera un poco más eficiente para cadenas más largas y, por lo tanto, solo tardó aproximadamente tres veces más. Una vez que la sobrecarga de la llamada a la función se vuelve insignificante en cuanto a la longitud de una cadena que estamos creando, en realidad tomará cuatro veces más tiempo crear una cadena dos veces más larga.

(Nota histórica: este análisis no se aplica necesariamente a las cadenas en el código fuente, por ejemplo html = 'abcd\n' + 'efgh\n' + ... + 'xyz.\n', dado que el compilador del código fuente de JavaScript puede unir las cadenas antes de convertirlas en un objeto de cadena de JavaScript. Hace solo unos años, la implementación de KJS de JavaScript se congelará o bloqueará al cargar largas cadenas de código fuente unidas por signos más. Desde el momento de la computación O(N^2)no fue difícil crear páginas web que sobrecargaran el navegador web Konqueror o Safari, que usaban el núcleo del motor KJS JavaScript. Me encontré con este problema cuando estaba desarrollando un lenguaje de marcado y un analizador de lenguaje de marcado JavaScript, y luego descubrí qué estaba causando el problema cuando escribí mi script para JavaScript Incluye.)

Claramente, esta rápida degradación del rendimiento es un gran problema. ¿Cómo podemos lidiar con eso, dado que no podemos cambiar la forma en que JavaScript maneja las cadenas como objetos inmutables? La solución es usar un algoritmo que recrea la cadena lo menos posible.

Para aclarar, nuestro objetivo es evitar agregar cadenas cortas a cadenas largas, ya que para agregar la cadena corta, también se debe duplicar toda la cadena larga.

Cómo funciona el algoritmo para evitar agregar cadenas cortas a cadenas largas

Aquí hay una buena manera de reducir la cantidad de veces que se crean nuevos objetos de cadena. Concatene longitudes de cadena más largas juntas de modo que se agregue más de un byte a la vez a la salida.

Por ejemplo, para hacer una cadena de longitud 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 */

Hacer esto requirió crear una cadena de longitud 1, crear una cadena de longitud 2, crear una cadena de longitud 4, crear una cadena de longitud 8 y, finalmente, crear una cadena de longitud 9. ¿Cuánto hemos ahorrado?

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

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

Tenga en cuenta que tuvimos que agregar una cadena de longitud 1 a una cadena de longitud 0, luego una cadena de longitud 1 a una cadena de longitud 1, luego una cadena de longitud 2 a una cadena de longitud 2, luego una cadena de longitud 4 a una cadena de longitud 4, luego una cadena de longitud 8 a una cadena de longitud 1, para obtener una cadena de longitud 9. Lo que estamos haciendo puede resumirse como evitar agregar cadenas cortas a cadenas largas, o en otro palabras, tratando de concatenar cadenas que son de igual o casi igual longitud.

Para el antiguo costo computacional encontramos una fórmula N(N+1)/2. ¿Existe una fórmula para el nuevo costo? Sí, pero es complicado. Lo importante es que lo es O(N), por lo que duplicar la longitud de la cadena duplicará aproximadamente la cantidad de trabajo en lugar de cuadruplicarlo.

El código que implementa esta nueva idea es casi tan complicado como la fórmula del costo computacional. Cuando lo lea, recuerde que >>= 1significa desplazarse a la derecha en 1 byte. Entonces, si n = 10011es un número binario, entonces n >>= 1da como resultado el valor n = 1001.

La otra parte del código que tal vez no reconozca es el bit a bit y el operador, escrito &. La expresión n & 1evalúa verdadero si el último dígito binario de nes 1, y falso si el último dígito binario de nes 0.

Nueva altamente eficientes en stringFill3()función de

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

Parece feo a simple vista, pero su rendimiento es nada menos que encantador.

Veamos qué tan bien funciona esta función. Después de ver los resultados, es probable que nunca olvide la diferencia entre un O(N^2)algoritmo y un O(N)algoritmo.

stringFill1()toma 88.7 microsegundos (millonésimas de segundo) para crear una cadena de 200 bytes, stringFill2()toma 62.54 y stringFill3()toma solo 4.608. ¿Qué hizo que este algoritmo fuera mucho mejor? Todas las funciones aprovecharon el uso de variables de función local, pero aprovechar las técnicas de optimización segunda y tercera agregó una mejora de veinte veces al rendimiento de stringFill3().

Análisis más profundo

¿Qué hace que esta función particular expulse a la competencia del agua?

Como he mencionado, la razón por la que ambas funciones stringFill1()y su stringFill2()ejecución son tan lentas es que las cadenas de JavaScript son inmutables. La memoria no se puede reasignar para permitir que se agregue un byte más a la vez a los datos de cadena almacenados por JavaScript. Cada vez que se agrega un byte más al final de la cadena, la cadena completa se regenera de principio a fin.

Por lo tanto, para mejorar el rendimiento del script, uno debe calcular previamente cadenas de mayor longitud concatenando dos cadenas juntas antes de tiempo, y luego acumulando recursivamente la longitud de cadena deseada.

Por ejemplo, para crear una cadena de bytes de 16 letras, primero se calculará previamente una cadena de dos bytes. Luego, la cadena de dos bytes se reutilizaría para calcular previamente una cadena de cuatro bytes. Luego, la cadena de cuatro bytes se reutilizaría para calcular previamente una cadena de ocho bytes. Finalmente, dos cadenas de ocho bytes se reutilizarían para crear la nueva cadena deseada de 16 bytes. En total, se tuvieron que crear cuatro cadenas nuevas, una de longitud 2, una de longitud 4, una de longitud 8 y una de longitud 16. El costo total es 2 + 4 + 8 + 16 = 30.

A la larga, esta eficiencia se puede calcular sumando en orden inverso y utilizando una serie geométrica que comience con un primer término a1 = N y que tenga una relación común de r = 1/2. La suma de una serie geométrica viene dada por a_1 / (1-r) = 2N.

Esto es más eficiente que agregar un carácter para crear una nueva cadena de longitud 2, crear una nueva cadena de longitud 3, 4, 5, y así sucesivamente, hasta 16. El algoritmo anterior utilizó ese proceso de agregar un solo byte a la vez , y el costo total sería n (n + 1) / 2 = 16 (17) / 2 = 8 (17) = 136.

Obviamente, 136 es un número mucho mayor que 30, por lo que el algoritmo anterior toma mucho, mucho más tiempo para construir una cadena.

Para comparar los dos métodos, puede ver qué tan rápido es el algoritmo recursivo (también llamado "divide y vencerás") en una cadena de longitud 123,457. En mi computadora FreeBSD, este algoritmo, implementado en la stringFill3()función, crea la cadena en 0.001058 segundos, mientras que la stringFill1()función original crea la cadena en 0.0808 segundos. La nueva función es 76 veces más rápida.

La diferencia en el rendimiento aumenta a medida que la longitud de la cadena se hace más grande. En el límite a medida que se crean cadenas cada vez más grandes, la función original se comporta aproximadamente como C1(constantes) veces N^2, y la nueva función se comporta como C2(constantes) veces N.

De nuestro experimento podemos determinar el valor de C1ser C1 = 0.0808 / (123457)2 = .00000000000530126997y el valor de C2ser C2 = 0.001058 / 123457 = .00000000856978543136. En 10 segundos, la nueva función podría crear una cadena que contiene 1,166,890,359 caracteres. Para crear esta misma cadena, la función anterior necesitaría 7.218.384 segundos de tiempo.

¡Esto es casi tres meses en comparación con diez segundos!

Solo respondo (varios años tarde) porque mi solución original a este problema ha estado flotando en Internet durante más de 10 años, y aparentemente los pocos que la recuerdan todavía no la entienden. Pensé que al escribir un artículo al respecto aquí ayudaría:

Optimizaciones de rendimiento para JavaScript de alta velocidad / Página 3

Desafortunadamente, algunas de las otras soluciones presentadas aquí todavía son algunas de las que tomarían tres meses para producir la misma cantidad de salida que una solución adecuada crea en 10 segundos.

Quiero tomarme el tiempo de reproducir parte del artículo aquí como respuesta canónica en Stack Overflow.

Tenga en cuenta que el algoritmo de mejor rendimiento aquí se basa claramente en mi algoritmo y probablemente fue heredado de la adaptación de tercera o cuarta generación de otra persona. Desafortunadamente, las modificaciones resultaron en la reducción de su rendimiento. La variación de mi solución presentada aquí tal vez no entendió mi for (;;)expresión confusa que se parece al bucle infinito principal de un servidor escrito en C, y que simplemente fue diseñada para permitir una declaración de interrupción cuidadosamente posicionada para el control del bucle, la forma más compacta de evite replicar exponencialmente la cadena un tiempo innecesario adicional.

Joseph Myers
fuente
44
Esta respuesta no debería haber recibido tantos votos a favor. En primer lugar, las afirmaciones de propiedad de Joseph son ridículas. El algoritmo subyacente tiene 3700 años.
artistoex
2
En segundo lugar, contiene mucha información errónea. Las implementaciones modernas de Javascript ni siquiera tocan el contenido de una cadena al realizar la concatenación (v8 representa cadenas concatenadas como un objeto de tipo ConsString). Todas las mejoras restantes son insignificantes (en términos de complejidad asintótica).
artistoex
3
Su idea de cómo se concatenan las cadenas es incorrecta. Para concatenar dos cadenas, Javascript no lee los bytes de las cadenas constituyentes. En cambio, simplemente crea un objeto que se refiere a las porciones izquierda y derecha. Es por eso que la última concatenación en el ciclo no es más costosa que las primeras.
artistoex
3
Por supuesto, esto resulta en un costo mayor que O (1) para indexar la cadena, por lo que la concatenación se puede aplanar más adelante, lo que de hecho merece una evaluación adicional.
artistoex
1
Esta fue una excelente lectura. ¡Deberías escribir un libro sobre eficiencia y todo eso!
39

Este es bastante eficiente

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
fuente
11
@Olegs, creo que la idea de votar es menos que votar por una persona o por la creatividad de una persona (lo que de hecho es aplaudible), pero la idea es votar por la solución más completa, de modo que se pueda encontrar fácilmente en el en la parte superior de la lista, sin tener que leer todas las respuestas en la búsqueda de la respuesta perfecta. (Porque, desafortunadamente, todos tenemos tiempo limitado ...)
Sorin Postelnicu
38

¡Buenas noticias! ahoraString.prototype.repeat es parte de JavaScript .

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

El método es compatible con todos los principales navegadores, excepto Internet Explorer y Android Webview. Para obtener una lista actualizada, consulte MDN: String.prototype.repeat> Compatibilidad del navegador .

MDN tiene un polyfill para navegadores sin soporte.

André Laszlo
fuente
Gracias por informar sobre el estado actual de las cosas, aunque creo que el polyfill de Mozilla es muy complicado para la mayoría de las necesidades (supongo que intentan imitar el comportamiento eficiente de su implementación de C), por lo que realmente no responderá al requisito de concisión del OP. Cualquiera de los otros enfoques configurados como polyfill será más conciso ;-)
Guss
2
¡Seguro! Pero usar el incorporado debe ser la versión más concisa. Dado que los polyfills son básicamente puertos secundarios, tienden a ser un poco complejos para garantizar la compatibilidad con las especificaciones (o especificaciones propuestas, en este caso). Lo agregué para completar, depende del OP decidir qué método usar, supongo.
André Laszlo
17

Ampliando la solución de P.Bailey :

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

De esta manera, debe estar a salvo de los tipos de argumentos inesperados:

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

EDITAR: ¡Créditos a Jerone por su elegante ++numidea!

Antichris
fuente
2
Cambió su es un poco:String.prototype.repeat = function(n){return new Array(isNaN(n) ? 1 : ++n).join(this);}
jerone
De todos modos, de acuerdo con esta prueba ( jsperf.com/string-repeat/2 ), hacer un bucle simple con concatenación de cadenas parece ser mucho más rápido en Chrome en comparación con el uso de Array.join. ¿No es gracioso?
Marco Demaio
8

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

Kalpesh Patel
fuente
Me gusta esto. No sé por qué no está ahí arriba.
Joe Thomas
quiéralo. tan simple y limpio
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
};

Así es como se repite la cadena varias veces con delimitador.

BitOfUniverse
fuente
4

Aquí hay una mejora del 5-7% en la respuesta de disfated.

Desenrolle el bucle deteniéndose en count > 1y realice una result += pattnernconcat adicional después del bucle. Esto evitará que los bucles finales no pattern += patternhayan sido utilizados previamente sin tener que usar un costoso if-check. El resultado final se vería así:

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

Y aquí está el violín de disfated bifurcado para la versión desenrollada: http://jsfiddle.net/wsdfg/

Dennis
fuente
2
function repeat(s, n) { var r=""; for (var a=0;a<n;a++) r+=s; return r;}
Joel Coehoorn
fuente
2
¿No es costosa la concatenación de cadenas? Ese es al menos el caso en Java.
Vijay Dev
¿Por qué sí lo son? Sin embargo, en realidad no se puede optimizar en javarscript. :(
McTrafik
Qué pasa con este rendimiento improvisado: var r=s; for (var a=1;...:)))) De todos modos, de acuerdo con esta prueba ( jsperf.com/string-repeat/2 ), hacer un bucle simple para concatenación de cadenas como lo que sugirió parece ser mucho más rápido en Chrome en comparación con el uso de Array .unirse.
Marco Demaio
@VijayDev - no de acuerdo con esta prueba: jsperf.com/ultimate-concat-vs-join
jbyrd
2

Pruebas de los distintos métodos:

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
fuente
2

Aquí está la versión segura de JSLint

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

Para todos los navegadores

Esto es lo más conciso posible:

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

Si también te importa el rendimiento, este es un enfoque mucho mejor:

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

Si desea comparar el rendimiento de ambas opciones, consulte este Fiddle y este Fiddle para las pruebas de referencia. ¡Durante mis propias pruebas, la segunda opción fue aproximadamente 2 veces más rápida en Firefox y aproximadamente 4 veces más rápida en Chrome!

Solo para navegadores modernos:

En los navegadores modernos, ahora también puede hacer esto:

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

Esta opción no solo es más corta que las otras dos, sino que es incluso más rápida que la segunda opción.

Desafortunadamente, no funciona en ninguna versión de Internet Explorer. Los números en la tabla especifican la primera versión del navegador que soporta totalmente este método :

ingrese la descripción de la imagen aquí

John Slegers
fuente
2
function repeat(pattern, count) {
  for (var result = '';;) {
    if (count & 1) {
      result += pattern;
    }
    if (count >>= 1) {
      pattern += pattern;
    } else {
      return result;
    }
  }
}

Puedes probarlo en JSFiddle . El punto de referencia contra el hacky Array.joiny el mío es, en términos generales, 10 (Chrome) a 100 (Safari) a 200 (Firefox) veces más rápido (dependiendo del navegador).

nikolay
fuente
2

Solo otra función de repetición:

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

ES2015Se ha realizado este repeat()método!

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/ Cadena / repetir
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
fuente
1

Este puede ser el recursivo más pequeño:

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

Concatenación recursiva simple

Solo quería darle una fiesta e hice esto:

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

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

No puedo decir que lo haya pensado mucho, y probablemente muestra :-)

Esto es posiblemente mejor

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

"foo".ditto( 128 );

Y es muy parecido a una respuesta ya publicada: lo sé.

Pero, ¿por qué ser recursivo?

¿Y qué tal un pequeño comportamiento predeterminado también?

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

"foo".ditto();

Porque , aunque el método no recursivo manejará repeticiones arbitrariamente grandes sin alcanzar los límites de la pila de llamadas, es mucho más lento.

¿Por qué me molesté en agregar más métodos que no son la mitad de inteligentes que los ya publicados?

En parte para mi propia diversión, y en parte para señalar de la manera más simple, sé que hay muchas maneras de desollar a un gato, y dependiendo de la situación, es muy posible que el método aparentemente mejor no sea el ideal.

Un método relativamente rápido y sofisticado puede bloquearse y quemarse efectivamente bajo ciertas circunstancias, mientras que un método más lento y simple puede hacer el trabajo, eventualmente.

Algunos métodos pueden ser poco más que explota, y como tal propenso a ser fijo fuera de la existencia, y otros métodos pueden funcionar muy bien en todas las condiciones, pero están construidos de manera que uno simplemente tiene ni idea de cómo funciona.

"¿Y qué si no sé cómo funciona?"

¿Seriamente?

JavaScript tiene uno de sus mayores puntos fuertes; es muy tolerante con el mal comportamiento, y tan flexible que se doblará hacia atrás para devolver resultados, ¡cuando podría haber sido mejor para todos si se hubiera roto!

"Con gran poder, viene una gran responsabilidad" ;-)

Pero más en serio e importante, aunque las preguntas generales como esta conducen a la genialidad en forma de respuestas inteligentes que, si nada más, expanden el conocimiento y los horizontes, al final, la tarea en cuestión, el guión práctico que utiliza el método resultante. puede requerir un poco menos o un poco más inteligente de lo que se sugiere.

Estos algoritmos "perfectos" son divertidos y todo, pero "una talla para todos" rara vez será mejor que hecho a medida.

Este sermón fue presentado por cortesía de la falta de sueño y un interés pasajero. ¡Sal y codifica!

Fred Gandt
fuente
1

En primer lugar, las preguntas del OP parecen ser sobre concisión, lo que entiendo que significa "simple y fácil de leer", mientras que la mayoría de las respuestas parecen ser sobre eficiencia, lo que obviamente no es lo mismo y también creo que a menos que implemente algo muy Algoritmos específicos de manipulación de datos grandes, no debería preocuparle cuando implemente funciones JavaScript de manipulación de datos básicos. La concisión es mucho más importante.

En segundo lugar, como observó André Laszlo, String.repeat es parte de ECMAScript 6 y ya está disponible en varias implementaciones populares, por lo que la implementación más concisa String.repeates no implementarlo ;-)

Por último, si necesita admitir hosts que no ofrecen la implementación ECMAScript 6, el polyfill de MDN mencionado por André Laszlo es todo menos conciso.

Entonces, sin más preámbulos, aquí está mi polyfill conciso :

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

Sí, esto es una recursión. Me gustan las recurrencias: son simples y si se hacen correctamente son fáciles de entender. En cuanto a la eficiencia, si el lenguaje lo admite, pueden ser muy eficientes si se escriben correctamente.

Según mis pruebas, este método es ~ 60% más rápido que el Array.joinenfoque. Aunque obviamente no se acerca a la implementación de disfated, es mucho más simple que ambos.

Mi configuración de prueba es el nodo v0.10, que utiliza el "Modo estricto" (creo que permite algún tipo de TCO ), solicitando repeat(1000)una cadena de 10 caracteres un millón de veces.

Guss
fuente
1

Si cree que todas esas definiciones de prototipos, creaciones de matrices y operaciones de unión son excesivas, simplemente use un código de línea único donde lo necesite. Cadena S que se repite N veces:

for (var i = 0, result = ''; i < N; i++) result += S;
Semra
fuente
3
El código debe ser legible. Si literalmente solo lo va a usar una vez, formatee correctamente (o use el Array(N + 1).join(str)método si no es un cuello de botella de rendimiento). Si existe la menor posibilidad de que lo vaya a usar dos veces, muévalo a una función con el nombre apropiado.
cloudfeet
1

Use Lodash para la funcionalidad de la utilidad Javascript, como repetir cadenas.

Lodash ofrece un rendimiento agradable y compatibilidad con ECMAScript.

Lo recomiendo para el desarrollo de UI y también funciona bien en el lado del servidor.

Aquí se explica cómo repetir la cadena "yo" 2 veces con Lodash:

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

Solución recursiva usando divide y vencerás:

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
fuente
0

Vine aquí al azar y nunca tuve una razón para repetir un char en javascript antes.

Me impresionó la forma en que artistoex lo hizo y los resultados desvalorizados. Noté que la última cadena de concat era innecesaria, como también señaló Dennis.

Noté algunas cosas más cuando jugué con la muestra desfavorecida junta.

Los resultados variaron bastante a menudo favoreciendo la última carrera y algoritmos similares a menudo competirían por la posición. Una de las cosas que cambié fue en lugar de usar el conteo generado por JSLitmus como semilla para las llamadas; Como el recuento se generó de manera diferente para los diversos métodos, puse un índice. Esto hizo la cosa mucho más confiable. Luego miré para asegurar que se pasaran cadenas de diferentes tamaños a las funciones. Esto evitó algunas de las variaciones que vi, donde algunos algoritmos funcionaron mejor en caracteres únicos o cadenas más pequeñas. Sin embargo, los 3 métodos principales funcionaron bien independientemente del tamaño de la cadena.

Conjunto de prueba bifurcado

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

Luego incluí el arreglo de Dennis y decidí ver si podía encontrar una manera de buscar un poco más.

Dado que javascript realmente no puede optimizar las cosas, la mejor manera de mejorar el rendimiento es evitar las cosas manualmente. Si tomé los primeros 4 resultados triviales fuera del ciclo, podría evitar 2-4 almacenes de cadenas y escribir el almacén final directamente en el resultado.

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

Esto resultó en una mejora del 1-2% en promedio sobre la corrección de Dennis. Sin embargo, diferentes ejecuciones y diferentes navegadores mostrarían una variación lo suficientemente justa como para que este código adicional no valga la pena el esfuerzo sobre los 2 algoritmos anteriores.

Una tabla

Editar: Hice esto principalmente bajo cromo. Firefox e IE a menudo favorecerán a Dennis en un par de%.

Andrew Hallendorff
fuente
0

Método simple:

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

Las personas complican demasiado esto hasta un punto ridículo o desperdician el rendimiento. Matrices? Recursividad? Tienes que estar bromeando.

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

Editar. Realicé algunas pruebas simples para comparar con la versión bit a bit publicada por artistoex / disfated y un montón de otras personas. Este último era solo marginalmente más rápido, pero con órdenes de magnitud más eficientes en memoria. Para 1000000 repeticiones de la palabra 'bla', el proceso de Nodo subió a 46 megabytes con el algoritmo de concatenación simple (arriba), pero solo 5.5 megabytes con el algoritmo logarítmico. Este último es definitivamente el camino a seguir. Reposicionándolo por razones de claridad:

function repeat (string, times) {
  var result = ''
  while (times > 0) {
    if (times & 1) result += string
    times >>= 1
    string += string
  }
  return result
}
Nelo Mitranim
fuente
Tienes una string += stringmitad de tiempo redundante .
nikolay
0

Concatenando cadenas basadas en un número.

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

¡Espero que ayude!

loxsat
fuente
0

Con ES8 también podría usar padStarto padEndpara esto. p.ej.

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