假设 - 根据 Matt Burland 的回答 -
通过给定算法之一创建长度为 n 的字符串的时间成本是
以复制的字符数为主,
观察到的时间可以通过计算这两种算法来很大程度上解释。
这产生 O(n2) 和 O(n log n) 并且对于长度为 10,000 的比率348:1。该算法在 Java 中可能会改进为 O(n),但在 .NET 中显然不会。
改进算法的成本
对改进算法的检验表明,复制的字符数c(n)服从如下递推关系:
c(0) = 0
c(1) = 1
c(n em>) = c(⌊n/2⌋) + c(⌈n/2⌉) + n
这个问题可以解决
c(2k + a) = (k + 1 )2k + (k + 3)a
选择 k 和 a 以使 n = 2k + a , a k ;这很容易通过完全归纳来验证。
这是O(k 2k),即O(n log2n), 即 O(n log n),
说明:成本比较
原算法清晰地复制了n(n+1)/2个字符,因此为O(n2)。
修改后的算法明显减少了复制的字符;
对于给定的 10,000 个字符串:
c(10000) =
c(213 + 1808) =
(13+1) * 8192 + 16 * 1808 =
143,616
原始算法复制 50,005,000 个字符,比例约为 1 : 348,
与观察到的 1:250 的比率在一个数量级内完全一致。
不完美的匹配确实表明内存管理等其他因素可能很重要。
进一步优化
假设字符串是用单个字符填充的,
假设 String.Substring 不会复制字符串,
根据comparison-of-substring-operation-performance-between-net-and-java,这在 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);
}
对于这个算法复制的字符数c2(n),我们有
c2(n) = n + c 2(⌈n/2⌉)
我们可以从中得到
c2(n) = 2_n_ + d(n)
如果 n 是 2 的幂,则 d(n) 为 -1,否则为“内部”(即既不是前导也不是尾随)位数相等在 m 的二进制展开中为 0;
等效地,d(n) 由 m ∈ ℕ in:
的第一个匹配案例定义
d(2m) = -1
d(2 m) = d(m)
d(m) = 基本数(非前导)m
中的 0 个二进制数字
c2 的表达式也可以通过完全归纳来验证,并且是 O(n + log n),即 O(n)。
从这个算法中移除递归是相当简单的。
在 OP 的情况下,此算法复制
c2(10,000) = 20,000 + d(110000110101000002) = 20,006 个字符
因此看起来会快 7 倍。
其他说明
- 此分析适用于创建任意字符串的倍数,而不仅仅是
"5"。
- 构造 OP 字符串的最有效方式大概是
String('5', ite)。
- 如果使用
StringBuilder 构建已知大小的字符串,可以使用StringBuilder(capacity) 来减少分配。
- 此分析适用于 .NET 以外的其他环境。
- 在 C 中,分配一个大小合适的缓冲区(包括
'\0'!),复制要重复的字符串,然后重复附加缓冲区已填充部分的副本,直到填满为止。