我使用以下代码追加字符串
string res = string.Empty;
int ite = 100000;
for(int i= 0; i < ite; i++)
{
res += "5";
}
这花费了很多时间,所以我后来将代码更改为
string res = string.Empty;
int ite = 100000;
res = getStr(ite / 2) + getStr(ite - (ite / 2));
//body of getStr method
private static string getStr(int p)
{
if (p == 1)
return "5";
else if (p == 0)
return string.Empty;
string r1 = getStr(p / 2); //recursive
string r2 = getStr(p - (p / 2)); //recursive
return (r1 + r2);
}
在我看来,实际上没有做任何事情,因为字符串连接的次数与之前的方法大致相同。
但是使用这种方法可以显着提高性能,因为代码大约需要2500毫秒(在我的机器上),现在需要10毫秒。
我在cpu时间运行了一个分析器,无法理解为什么性能会有所改善。任何人都可以解释一下。
注意:我故意不使用StringBuilder,以便了解上述内容。
答案 0 :(得分:15)
你需要考虑为什么字符串连接很慢。字符串是不可变的,所以当你这样做时:
someString+= "5";
您必须将someString
的整个内容复制到另一个较大的字符串,然后复制到5
部分。如果你考虑一下,字符串得到的时间越长越慢。
使用递归函数,您可以采用分而治之的策略来帮助最大限度地减少所需的大字符串连接数。例如,如果您的长度为8,则在第一种情况下您将执行:
"5" + "5"
"55" + "5"
"555" + "5"
"5555" + "5"
"55555" + "5"
"555555" + "5"
"5555555" + "5" // 7 total concatenations
在您正在进行的递归模型中:
"5" + "5" // Four times
"55" + "55" // twice
"5555" + "5555" // once
所以你没有那么大的连接。
当然,我认为OP从他们的评论中知道这一点,但对其他人来说;如果您需要连接任何非平凡数量的字符串,请使用StringBuilder,因为它是为构建字符串而优化的Append
。
答案 1 :(得分:0)
假设 - 根据Matt Burland的回答 - 通过给定算法之一创建长度为 n 的字符串的时间成本是 由复制的字符数量占主导地位, 观察到的时间可以通过计算两种算法来解释。 这产生O( n 2 )和O( n log n ),对于长度为10,000的比率348:1。该算法可以在Java中改进为O( n ),但显然不在.NET中。
对改进算法的检查表明,复制的字符数 c ( n )遵循以下递归关系:
c (0)= 0
c (1)= 1
c ( n )= c(⌊ n /2⌋)+ c(⌈ n /2⌉) + n
这可以解决产生
c (2 k + a )=( k + 1 )2 k +( k + 3)a
选择 k 和 a ,以便 n = 2 k + a , a &lt; 2 k ;这很容易通过完全归纳验证。 这是O( k 2 k ),即O( n log 2 n ),即O( n log n ),
原始算法清楚地复制 n ( n +1)/ 2个字符,因此是O( n 2 < / SUP>)。
修改后的算法清晰地复制了更少的字符; 对于给定的10,000个字符串:
c (10000)=
c (2 13 + 1808)=
(13 + 1)* 8192 + 16 * 1808 =
143616
原始算法复制50,005,000个字符,比例约为1:348, 在观察到的比例为1:250时,一致到一个数量级。 不完美的匹配确实表明内存管理等其他因素可能很重要。
鉴于字符串填充了单个字符,
并且假设 String.Substring
没有复制字符串,
这在Java中是正确的,但根据comparison-of-substring-operation-performance-between-net-and-java,不是.NET ,
我们可以改进第二种算法(不使用StringBuilder
或String('5', ite)
)
通过不断加倍构造的字符串,在必要时添加额外的字符:
private static string getStr(int p)
{
if(p == 0)
return "";
if(p == 1)
return "5";
string s = getStr ((p+1) / 2);
if( s.Length + s.Length == p )
return s + s;
else
return s + s.Substring(0, p - s.Length);
}
对于此算法复制的字符数 c 2 ( n ),我们有
c 2 ( n )= n + c 2 子>(⌈名词的/2⌉)
我们可以从中得出
c 2 ( n )= 2_n_ + d( n )
其中d( n )是-1,如果 n 是2的幂,否则“内部”(即既不是前导也不是尾随)数字等于在 m 的二进制扩展中为0; 等价地,d( n )由 m ∈ℕ的第一个匹配情况定义:
d (2 m )= -1
中基本(非领先)0二进制数字的数量
d (2 m )= d ( m )
d ( m )= m
c 2 的表达式也可以通过完全归纳验证,并且是O( n + log n ),即O( n )。
从此算法中删除递归非常简单。
在OP的情况下,该算法复制 c 2 (10,000)= 20,000 + d(11000011010100000 2 )= 20,006个字符 因此看起来要快7倍。
"5"
。String('5', ite)
。StringBuilder
构建已知大小的字符串,则可以使用StringBuilder(capacity)
来减少分配。'\0'
!),复制到要重复的字符串中,然后重复附加缓冲区的填充部分的副本,直到它满了。