一个更整洁、更优雅的解决方案(我称其为“基本解决方案”)如下所示:
基本解决方案
char *internalRepeat(char *s, int n, size_t total)
{
return (n > 0)
? strcat(internalRepeat(s, n - 1, total + strlen(s)), s)
: strcpy(malloc(total + 1), "");
}
char *repeat(char *s, int n)
{
return internalRepeat(s, n, 0);
}
这就是递归的美妙之处。解决方案的关键在于使用递归逐步构建结果的长度。参数
total
完成此操作(不包括NUL终止符)。当递归终止时,结果缓冲区被一次性分配(包括NUL终止符),然后我们使用递归展开将每个
s
的副本附加到结果中。基本解决方案的行为如下:
- 对于空字符串的任意重复次数,返回零长度字符串。
- 对于非空字符串的零或负迭代,返回零长度字符串。
- 对于非零正数重复次数的非空字符串,返回非零长度字符串。
如果您根据上述函数创建一个程序,则以下语句:
printf("Repeat \"\" 0 times: [%s]\n", repeat("", 0));
printf("Repeat \"\" 3 times: [%s]\n", repeat("", 3));
printf("Repeat \"abcde\" 0 times: [%s]\n", repeat("abcde", 0));
printf("Repeat \"abcde\" 1 times: [%s]\n", repeat("abcde", 1));
printf("Repeat \"abcde\" 4 times: [%s]\n", repeat("abcde", 4));
将会产生以下输出:
Repeat "" 0 times: []
Repeat "" 3 times: []
Repeat "abcde" 0 times: []
Repeat "abcde" 1 times: [abcde]
Repeat "abcde" 4 times: [abcdeabcdeabcdeabcde]
编辑: 优化解决方案如下。如果您对优化技术感兴趣,请继续阅读。
这里所有的其他提议主要都是以O(n^2)为主,并在每次迭代时分配内存。尽管基本解决方案非常优雅,仅使用单个malloc()
,并且只需要两个语句,但出人意料的是,基本解决方案的运行时间也是O(n^2)。如果字符串s
很长,这将使它非常低效,并且意味着基本解决方案不比这里的任何其他提议更有效。
优化解决方案
以下是一个实际上以O(n)运行的最优解决方案:
char *internalRepeat(char *s, int n, size_t total, size_t len)
{
return (n > 0)
? strcpy(internalRepeat(s, n - 1, total, len), s) + len
: strcpy(malloc(total + 1), "");
}
char *repeat(char *s, int n)
{
int len = strlen(s);
return internalRepeat(s, n, n * len, len) - (n * len);
}
正如您所见,现在它有三个语句,并使用一个名为
len
的参数来缓存
s
的长度。它递归地使用
len
来计算结果缓冲区中第
n个
s
副本的位置,从而使我们能够将每次添加
s
到结果的
strcat()
替换为
strcpy()
。这给出了一个实际运行时间为O(
n),而不是O(
n^2)。
基本和优化方案之间有什么区别呢?
所有其他解决方案都至少使用了
n
次
strcat()
在字符串
s
上,以将
n
个
s
的副本附加到结果。这就是问题所在,因为
strcat()
的实现隐藏了一种低效性。在内部,
strcat()
可以被认为是:
strcat = strlen + strcpy
即,在追加时,您必须先找到要追加的字符串的末尾 然后 才能进行追加本身。这种隐藏开销意味着,实际上创建字符串的n
个副本需要n
个长度检查和n
个物理复制操作。然而,真正的问题在于对于每个s
的副本,我们追加的结果变得更长了。这意味着在strcat()
中每个连续的长度检查都在结果上变得更长了。如果我们现在将这两种解决方案使用“我们必须扫描或复制s
的次数”作为比较基础,我们可以看到这两种解决方案的差异所在。
对于字符串s
的n
个副本,基本解决方案的执行情况如下:
strlen's/iteration: 2
strcpy's/iteration: 1
Iteration | Init | 1 | 2 | 3 | 4 | ... | n | Total |
----------+------+---+---+---+---+-----+---+------------+
Scan "s" | 0 | 1 | 2 | 3 | 4 | ... | n | (n+1)(n/2) |
Copy "s" | 0 | 1 | 1 | 1 | 1 | ... | 1 | n |
而优化后的解决方案则像这样表现:
strlen's/iteration: 0
strcpy's/iteration: 1
Iteration | Init | 1 | 2 | 3 | 4 | ... | n | Total |
----------+------+---+---+---+---+-----+---+------------+
Scan "s" | 1 | 0 | 0 | 0 | 0 | ... | 0 | 1 |
Copy "s" | 0 | 1 | 1 | 1 | 1 | ... | 1 | n |
如表所示,由于
strcat()
中内置的长度检查,基本解决方案对我们的字符串执行 (
n^2 + n)/2 次扫描,而优化解决方案始终执行
(n + 1) 次扫描。这就是为什么基本解决方案(以及所有依赖于
strcat()
的其他解决方案)的执行时间为
O(n^2),而优化解决方案的执行时间为
O(n)。
在实际情况下,
O(n) 和
O(n^2) 的运行时间差异非常大,特别是当使用大型字符串时。例如,让我们取一个 1MB 的字符串
s
并创建它的 1,000 个副本(== 1GB)。如果我们有一颗每个时钟周期可以扫描或复制 1 字节的 1GHz CPU,那么将生成 1,000 个
s
副本如下:
注意:上面的性能表中取出的 n 表示对 s 进行单次扫描。
Basic: (n + 1) * (n / 2) + n = (n ^ 2) / 2 + (3n / 2)
= (10^3 ^ 2) / 2 + (3 * 10^3) / 2
= (5 * 10^5) + (1.5 * 10^2)
= ~(5 * 10^5) (scans of "s")
= ~(5 * 10^5 * 10^6) (bytes scanned/copied)
= ~500 seconds (@1GHz, 8 mins 20 secs).
Optimised: (n + 1) = 10^3 + 1
= ~10^3 (scans of "s")
= ~10^3 * 10^6 (bytes scanned/copied)
= 1 second (@1Ghz)
如您所见,优化后的解决方案几乎即时完成,而基本解决方案需要近10分钟才能完成。然而,如果您认为将字符串s
缩小会有帮助,那么下一个结果可能会让您感到震惊。同样,在一个处理器每个时钟周期处理1字节的1GHz计算机上,我们将s
设置为1KB(比原来小1000倍),并复制1,000,000次(总共==1GB,与之前相同)。这给出:
Basic: (n + 1) * (n / 2) + n = (n ^ 2) / 2 + (3n / 2)
= (10^6 ^ 2) / 2 + (3 * 10^6) / 2
= (5 * 10^11) + (1.5 * 10^5)
= ~(5 * 10^11) (scans of "s")
= ~(5 * 10^11 * 10^3) (bytes scanned/copied)
= ~50,000 seconds (@1GHz, 833 mins)
= 13hrs, 53mins, 20 secs
Optimised: (n + 1) = 10^6 + 1
= ~10^6 (scans of "s")
= ~10^6 * 10^3 (bytes scanned/copied)
= 1 second (@1Ghz)
这是一个非常惊人的差异。优化后的解决方案在写入的数据总量相同时与之前的表现相同。然而,基本解决方案需要停顿超过半天才能构建结果。这就是O(n)和O(n^2)运行时间之间的差异。
srcLength * n + 1
字节。你当前的计算不会为空终止符留出空间。 - interjayif
语句去掉,简化一下代码。 - interjay