在 C# 中,用于增加 StringBuilder 使用的内部缓冲区的策略随着时间的推移发生了变化。
解决这个问题有三种基本策略,它们具有不同的性能特征。
第一个基本策略是:
- 制作一个字符数组
- 当你用完空间时,创建一个包含更多字符的新数组,用于某个常数 k。
- 将旧数组复制到新数组,并孤立旧数组。
这种策略有很多问题,其中最明显的是,如果正在构建的字符串非常大,则时间为 O(n 2 )。假设 k 是一千个字符,最终的字符串是一百万个字符。您最终将字符串重新分配到 1000、2000、3000、4000...,因此复制了 1000 + 2000 + 3000 + 4000 + ... + 999000 个字符,总计复制了 5000 亿个字符!
这种策略有一个很好的特性,即“浪费”的内存量以 k 为界。
在实践中,由于存在 n 平方问题,这种策略很少使用。
第二个基本策略是
- 做一个数组
- 当你的空间用完时,创建一个包含更多字符的新数组,用于某个常数 k。
- 将旧数组复制到新数组,并孤立旧数组。
k% 通常为 100%;如果是,那么这就是所谓的“双满”策略。
该策略具有很好的特性,即其摊销成本为 O(n)。再次假设最终字符串是一百万个字符,而您从一千个字符开始。您以 1000、2000、4000、8000... 复制,最终复制 1000 + 2000 + 4000 + 8000 ... + 512000 个字符,总计复制了大约 100 万个字符;好多了。
该策略的特性是,无论您选择什么百分比,摊销成本都是线性的。
这种策略有许多缺点,有时复制操作非常昂贵,并且您可能会在未使用的内存中浪费多达 k% 的最终字符串长度。
第三种策略是制作一个数组链表,每个数组的大小为 k。当您溢出现有数组时,会分配一个新数组并将其附加到列表的末尾。
这种策略有一个很好的特性,即没有任何操作特别昂贵,总浪费的内存以 k 为界,并且您不需要能够定期在堆中定位大块。它的缺点是最终将事物转换为字符串可能会很昂贵,因为链表中的数组可能具有较差的局部性。
.NET 框架中的字符串生成器曾经使用双时满策略;它现在使用块链表策略。