一个更整洁优雅的解决方案(我称之为基本解决方案)如下:
基本解决方案
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'th 副本s 将被定位的位置,因此允许我们在每次添加s 时将strcat() 替换为strcpy()到结果。这给出了 O(n) 的实际运行时间,而不是 O(n^2)。
基本解决方案和优化解决方案有什么区别?
所有其他解决方案在字符串s 上至少使用了strcat() 次n 以将n 的s 副本附加到结果中。这就是问题所在,因为strcat() 的实现隐藏了低效率。在内部,strcat() 可以认为是:
strcat = strlen + strcpy
即,在追加时,您首先必须找到要追加到的字符串的结尾,之前您可以自己进行追加。这种隐藏的开销意味着,事实上,创建字符串的n 副本需要n 长度检查和n 物理复制操作。然而,真正的问题在于,对于我们附加的每个 s 副本,我们的结果会变得更长。这意味着在 result 上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) 的副本。如果我们有一个 1GHz CPU 可以扫描或复制 1 个字节/时钟周期,那么将生成 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) 之间运行时间的差异。