返回重复任意次数的字符串的最佳或最简洁的方法是什么?
以下是我迄今为止最好的镜头:
function repeat(s, n){
var a = [];
while(a.length < n){
a.push(s);
}
return a.join('');
}
返回重复任意次数的字符串的最佳或最简洁的方法是什么?
以下是我迄今为止最好的镜头:
function repeat(s, n){
var a = [];
while(a.length < n){
a.push(s);
}
return a.join('');
}
新读者请注意:这个答案很旧而且不太实用 - 它只是“聪明”,因为它使用 Array 的东西来完成 String 的事情。当我写“更少的过程”时,我的意思绝对是“更少的代码”,因为正如其他人在随后的答案中指出的那样,它的表现就像一头猪。因此,如果速度对您很重要,请不要使用它。
我会直接把这个函数放到 String 对象上。而不是创建一个数组,填充它,并用一个空字符连接它,只需创建一个适当长度的数组,然后用你想要的字符串连接它。同样的结果,更少的过程!
String.prototype.repeat = function( num )
{
return new Array( num + 1 ).join( this );
}
alert( "string to repeat\n".repeat( 4 ) );
我已经测试了所有建议方法的性能。
这是我得到的最快的变体。
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;
};
或作为独立功能:
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;
}
它基于artistoex算法。它真的很快。越大count
,与传统new Array(count + 1).join(string)
方法相比,它运行得越快。
我只改变了两件事:
pattern = this
为pattern = this.valueOf()
(清除一个明显的类型转换);if (count < 1)
从prototypejs 的检查以排除在这种情况下不必要的操作。UPD
在这里为那些有兴趣的人创建了一个小小的性能测试游乐场。
变量count
~ 0 .. 100:
常数count
= 1024:
如果可以,请使用它并使其更快:)
这个问题是 JavaScript 的一个众所周知的/“经典”优化问题,因为 JavaScript 字符串是“不可变的”,并且通过将单个字符连接到字符串的添加需要创建,包括内存分配和复制到, 一个全新的字符串。
不幸的是,此页面上接受的答案是错误的,其中“错误”的意思是简单的单字符字符串的性能因子为 3 倍,重复多次的短字符串为 8x-97 倍,重复句子为 300 倍,当将算法复杂性比率的极限n
设为无穷大。此外,此页面上还有另一个几乎正确的答案(基于过去 13 年来在 Internet 上流传的正确解决方案的许多代和变体之一)。然而,这个“几乎正确”的解决方案错过了正确算法的关键点,导致性能下降 50%。
已接受答案的 JS 性能结果、表现最佳的其他答案(基于此答案中原始算法的降级版本),以及使用我 13 年前创建的算法的此答案
~ 2000 年 10 月,我针对这个确切的问题发布了一个算法,该算法被广泛改编、修改,但最终被人们理解和遗忘。为了解决这个问题,我在 2008 年 8 月发表了一篇文章http://www.webreference.com/programming/javascript/jkm3/3.html解释了算法并将其用作简单的通用 JavaScript 优化示例。到目前为止,Web Reference已经从这篇文章中删除了我的联系信息,甚至我的名字。再一次,该算法已经被广泛采用、修改,然后被理解得很少并且在很大程度上被遗忘了。
Joseph Myers 的原始字符串重复/乘法 JavaScript 算法,大约在 Y2K 作为 Text.js 中的文本乘法函数;2008 年 8 月以这种形式发表于 Web Reference:http : //www.webreference.com/programming/javascript/jkm3/3.html(文章以该函数作为 JavaScript 优化的例子,对于奇怪的名称“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;
}
在那篇文章发表后的两个月内,同样的问题被张贴到 Stack Overflow 上,直到现在,显然这个问题的原始算法再次被遗忘了。此 Stack Overflow 页面上可用的最佳解决方案是我的解决方案的修改版本,可能分隔几代。不幸的是,修改破坏了解决方案的最优性。事实上,通过改变我原来的循环结构,修改后的解决方案执行了一个完全不需要的指数复制的额外步骤(因此将正确答案中使用的最大字符串与自身连接额外的时间,然后将其丢弃)。
下面接着讨论一些 JavaScript 优化,这些优化与这个问题的所有答案相关,并为了所有人的利益。
为了说明这种技术是如何工作的,我们使用了一个现实生活中的 JavaScript 函数,它可以创建任意长度的字符串。正如我们将看到的,可以添加更多优化!
与此处使用的功能类似的功能是创建填充以对齐文本列、格式化货币或将块数据填充到边界。文本生成函数还允许使用可变长度输入来测试对文本进行操作的任何其他函数。该函数是 JavaScript 文本处理module的重要组成部分之一。
在我们继续进行的过程中,我们将涵盖另外两种最重要的优化技术,同时将原始代码开发为用于创建字符串的优化算法。最终的结果是一个工业强度、高性能的函数,我到处都在使用——在 JavaScript 订单、数据格式和电子邮件/文本消息格式以及许多其他用途中对齐项目价格和总计。
创建字符串的原始代码 stringFill1()
function stringFill1(x, n) {
var s = '';
while (s.length < n) s += x;
return s;
}
/* Example of output: stringFill1('x', 3) == 'xxx' */
语法在这里很清楚。如您所见,在进行更多优化之前,我们已经使用了局部函数变量。
请注意,代码中有一个对对象属性的无害引用s.length
会损害其性能。更糟糕的是,使用这个对象属性会假设读者知道 JavaScript 字符串对象的属性,从而降低了程序的简单性。
使用此对象属性破坏了计算机程序的通用性。程序假定x
必须是长度为 1 的字符串。这将stringFill1()
函数的应用限制为除了重复单个字符之外的任何内容。如果单个字符包含多个字节(如 HTML 实体),则甚至不能使用它们
。
这种不必要地使用对象属性导致的最严重问题是,如果在空输入字符串上进行测试,该函数会创建一个无限循环x
。要检查通用性,请将程序应用于尽可能少的输入。当被要求超过可用内存量时崩溃的程序有一个借口。像这样的程序在被要求不产生任何东西时崩溃是不可接受的。有时漂亮的代码是有毒的代码。
简单性可能是计算机编程的一个模棱两可的目标,但通常并非如此。当一个程序缺乏任何合理水平的通用性时,说“该程序就其发展而言已经足够好”是无效的。如您所见,使用该string.length
属性可防止该程序在一般设置下运行,事实上,错误的程序已准备好导致浏览器或系统崩溃。
有没有办法在提高这个 JavaScript 的性能的同时解决这两个严重的问题?
当然。只需使用整数。
用于创建字符串的优化代码 stringFill2()
function stringFill2(x, n) {
var s = '';
while (n-- > 0) s += x;
return s;
}
比较stringFill1()
和的时序代码stringFill2()
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);
迄今为止的成功 stringFill2()
stringFill1()
填充一个 100 字节的字符串需要 47.297 微秒(百万分之一秒),而stringFill2()
做同样的事情需要 27.68 微秒。通过避免对对象属性的引用,这几乎使性能翻了一番。
我们之前的结果看起来不错——事实上非常好。stringFill2()
由于使用了我们的前两个优化,改进后的功能要快得多。如果我告诉你它可以改进到比现在快很多倍,你会相信吗?
是的,我们可以实现这个目标。现在我们需要解释我们如何避免将短字符串附加到长字符串。
与我们的原始函数相比,短期行为似乎相当不错。计算机科学家喜欢分析函数或计算机程序算法的“渐近行为”,这意味着通过用更大的输入对其进行测试来研究其长期行为。有时不做进一步的测试,人们永远不会意识到可以改进计算机程序的方法。为了看看会发生什么,我们将创建一个 200 字节的字符串。
出现的问题 stringFill2()
使用我们的计时函数,我们发现 200 字节字符串的时间增加到 62.54 微秒,而 100 字节字符串的时间增加到 27.68 微秒。似乎做两倍的工作时间应该加倍,但实际上是三倍或四倍。从编程经验来看,这个结果似乎很奇怪,因为如果有的话,该函数应该稍微快一点,因为工作效率更高(每个函数调用 200 字节,而不是每个函数调用 100 字节)。这个问题与 JavaScript 字符串的一个阴险属性有关:JavaScript 字符串是“不可变的”。
不可变意味着一旦创建字符串就不能更改它。通过一次添加一个字节,我们不会再消耗一个字节的工作量。我们实际上是在重新创建整个字符串加上一个字节。
实际上,要在 100 字节的字符串中再添加一个字节,需要花费 101 字节的工作量。我们来简单分析一下创建N
字节串的计算成本。添加第一个字节的成本是 1 个计算单位。添加第二个字节的成本不是一个单位,而是 2 个单位(将第一个字节复制到新的字符串对象以及添加第二个字节)。第三个字节需要 3 个单位的成本,以此类推。
C(N) = 1 + 2 + 3 + ... + N = N(N+1)/2 = O(N^2)
. 该符号O(N^2)
发音为 Big O of N squared,这意味着从长远来看,计算成本与字符串长度的平方成正比。创建 100 个角色需要 10,000 个工作单位,创建 200 个角色需要 40,000 个工作单位。
这就是为什么创建 200 个字符比创建 100 个字符需要两倍多的时间。事实上,它应该花费四倍的时间。我们的编程经验是正确的,因为对于更长的字符串,这项工作的效率稍微提高了一点,因此只用了大约三倍的时间。一旦函数调用的开销对于我们创建的字符串的长度变得可以忽略不计,实际上创建两倍长的字符串将花费四倍的时间。
(历史注释:这种分析不一定适用于源代码中的字符串,例如html = 'abcd\n' + 'efgh\n' + ... + 'xyz.\n'
,因为 JavaScript 源代码编译器可以将字符串连接在一起,然后再将它们变成 JavaScript 字符串对象。就在几年前,KJS 实现JavaScript 在加载由加号连接的长串源代码时会死机或崩溃。由于计算时间,O(N^2)
因此制作使使用 KJS JavaScript 引擎核心的 Konqueror Web 浏览器或 Safari 过载的网页并不困难。我首先我在开发标记语言和 JavaScript 标记语言解析器时遇到了这个问题,然后我在为 JavaScript Includes 编写脚本时发现了导致问题的原因。)
显然,这种性能的快速下降是一个巨大的问题。鉴于我们无法改变 JavaScript 将字符串作为不可变对象处理的方式,我们该如何处理它?解决方案是使用尽可能少地重新创建字符串的算法。
澄清一下,我们的目标是避免将短字符串添加到长字符串,因为为了添加短字符串,还必须复制整个长字符串。
算法如何工作以避免将短字符串添加到长字符串
这是减少创建新字符串对象次数的好方法。将更长的字符串连接在一起,以便一次将多个字节添加到输出中。
例如,要制作一个长度为的字符串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 */
这样做需要创建一个长度为 1 的字符串,创建一个长度为 2 的字符串,创建一个长度为 4 的字符串,创建一个长度为 8 的字符串,最后创建一个长度为 9 的字符串。我们节省了多少成本?
旧成本C(9) = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 = 45
。
新成本C(9) = 1 + 2 + 4 + 8 + 9 = 24
。
请注意,我们必须将长度为 1 的字符串添加到长度为 0 的字符串,然后将长度为 1 的字符串添加到长度为 1 的字符串,然后将长度为 2 的字符串添加到长度为 2 的字符串,然后将长度为 4 的字符串到长度为4的字符串,然后长度为8的字符串到长度为1的字符串,以获得长度为9的字符串。 我们所做的可以概括为避免将短字符串添加到长字符串,或者其他词,试图将长度相等或几乎相等的字符串连接在一起。
对于旧的计算成本,我们找到了一个公式N(N+1)/2
。新的成本有公式吗?是的,但它很复杂。重要的是它是O(N)
,因此将字符串长度加倍将大约使工作量加倍而不是四倍。
实现这个新想法的代码几乎和计算成本的公式一样复杂。当您阅读它时,请记住这>>= 1
意味着右移 1 个字节。因此,如果n = 10011
是二进制数,则n >>= 1
结果为 value n = 1001
。
您可能不认识的另一部分代码是按位和运算符,写成&
. n & 1
如果 的最后一位二进制数n
为 1,则表达式为真,如果 的最后一位二进制数n
为 0 ,则表达式为假。
新的高效stringFill3()
功能
function stringFill3(x, n) {
var s = '';
for (;;) {
if (n & 1) s += x;
n >>= 1;
if (n) x += x;
else break;
}
return s;
}
在未经训练的人看来,它看起来很丑陋,但它的性能无异于可爱。
让我们看看这个函数的表现如何。看到结果后,您可能永远不会忘记O(N^2)
算法和O(N)
算法之间的区别。
stringFill1()
创建一个 200 字节的字符串需要 88.7 微秒(百万分之一秒),stringFill2()
需要 62.54,stringFill3()
只需要 4.608。是什么让这个算法如此出色?所有函数都利用了使用局部函数变量的优势,但利用第二和第三优化技术将 的性能提高了 20 倍stringFill3()
。
更深入的分析
是什么让这个特殊的功能在竞争中脱颖而出?
正如我所提到的,这两个函数stringFill1()
和stringFill2()
, 运行如此缓慢的原因是 JavaScript 字符串是不可变的。内存不能重新分配以允许一次多一个字节附加到 JavaScript 存储的字符串数据。每增加一个字节到字符串的末尾,整个字符串就会从头到尾重新生成。
因此,为了提高脚本的性能,必须通过提前将两个字符串连接在一起来预先计算更长的字符串,然后递归地构建所需的字符串长度。
例如,要创建一个 16 个字母的字节字符串,首先要预先计算一个两个字节的字符串。然后将重用两个字节的字符串来预先计算一个四字节的字符串。然后将重用四字节字符串来预先计算八字节字符串。最后,将重复使用两个 8 字节字符串来创建所需的 16 字节新字符串。总共必须创建四个新字符串,一个长度为 2,一个长度为 4,一个长度为 8,一个长度为 16。总成本为 2 + 4 + 8 + 16 = 30。
从长远来看,这种效率可以通过以相反的顺序相加并使用从第一项 a1 = N 开始并具有公共比率 r = 1/2 的几何级数来计算。几何级数的总和由 给出a_1 / (1-r) = 2N
。
这比添加一个字符以创建长度为 2 的新字符串、创建长度为 3、4、5 等直到 16 的新字符串更有效。 之前的算法使用的是一次添加一个字节的过程,其总成本为n (n + 1) / 2 = 16 (17) / 2 = 8 (17) = 136
。
显然,136 是一个比 30 大得多的数字,因此之前的算法需要更多的时间来构建一个字符串。
为了比较这两种方法,您可以看到递归算法(也称为“分而治之”)在长度为 123,457 的字符串上的速度有多快。在我的 FreeBSD 计算机上,此算法在stringFill3()
函数中实现,在 0.001058 秒内创建字符串,而原始stringFill1()
函数在 0.0808 秒内创建字符串。新功能的速度提高了 76 倍。
随着字符串的长度变大,性能差异也越来越大。在创建越来越大的字符串的限制中,原始函数的行为大致类似于C1
(constant) times N^2
,而新函数的行为类似于C2
(constant) times N
。
从我们的实验中,我们可以判断的valueC1
是C1 = 0.0808 / (123457)2 = .00000000000530126997
,和值C2
是C2 = 0.001058 / 123457 = .00000000856978543136
。在 10 秒内,新函数可以创建一个包含 1,166,890,359 个字符的字符串。为了创建相同的字符串,旧函数需要 7,218,384 秒的时间。
与十秒相比,这几乎是三个月!
我只是回答(晚了几年)因为我对这个问题的原始解决方案已经在互联网上流传了 10 多年,而且显然很少有人记得它。我认为通过在这里写一篇关于它的文章我会有所帮助:
不幸的是,这里介绍的一些其他解决方案仍然需要三个月才能产生与适当解决方案在 10 秒内产生的相同数量的输出。
我想花时间在这里复制部分文章作为 Stack Overflow 上的规范答案。
请注意,此处性能最佳的算法显然是基于我的算法,并且可能是从其他人的第 3 代或第 4 代改编版继承而来的。不幸的是,这些修改导致其性能下降。我在这里提出的解决方案的变体可能不理解我的令人困惑的for (;;)
表达,它看起来像用 C 编写的服务器的主要无限循环,并且它的设计只是为了允许仔细定位的 break 语句进行循环控制,这是最紧凑的方式避免以指数方式复制字符串一个额外的不必要的时间。
好消息!String.prototype.repeat
是现在的JavaScript的一部分。
"yo".repeat(2);
// returns: "yoyo"
除 Internet Explorer 外,所有主要浏览器都支持该方法。有关最新列表,请参阅MDN:String.prototype.repeat > 浏览器兼容性。
MDN 有一个不支持浏览器的 polyfill。
这个很有效
String.prototype.repeat = function(times){
var result="";
var pattern=this;
while (times > 0) {
if (times&1)
result+=pattern;
times>>=1;
pattern+=pattern;
}
return result;
};