字符串连接时的性能 - 算法字符串字符串c#

时间:2015-07-23 17:29:46

标签: c# string algorithm string-concatenation

我使用以下代码追加字符串

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,以便了解上述内容。

2 个答案:

答案 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 , 我们可以改进第二种算法(不使用StringBuilderString('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
   d (2 m )= d m
   d m )= m

中基本(非领先)0二进制数字的数量

c 2 的表达式也可以通过完全归纳验证,并且是O( n + log n ),即O( n )。

从此算法中删除递归非常简单。

在OP的情况下,该算法复制 c 2 (10,000)= 20,000 + d(11000011010100000 2 )= 20,006个字符 因此看起来要快7倍。

其他评论

  • 此分析适用于创建任意字符串的倍数,而不仅仅是"5"
  • 构建OP字符串的最有效方法大概是String('5', ite)
  • 如果使用StringBuilder构建已知大小的字符串,则可以使用StringBuilder(capacity)来减少分配。
  • 此分析适用于除.NET以外的其他环境。
  • 在C中,分配一个大小合适的缓冲区(包括'\0'!),复制到要重复的字符串中,然后重复附加缓冲区的填充部分的副本,直到它满了。