一个更整洁和优雅的解决方案(我称之为Basic Solution)如下:
基本解决方案
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
将被定位的位置,因此允许我们替换strcat()
为strcpy()
每次s
添加到结果中。这给出了O( n ) 的实际运行时间,而不是 O( n^2 )。
基本解决方案和优化解决方案有什么区别?
所有其他解决方案strcat()
至少n
在字符串上使用了多次以将副本s
附加到结果中。这就是问题所在,因为实施隐藏了低效率。在内部,可以认为是:n
s
strcat()
strcat()
strcat = strlen + strcpy
即,在追加时,您首先必须找到要追加的字符串的结尾,然后才能执行追加本身。这种隐藏的开销意味着,事实上,创建n
字符串的副本需要n
长度检查和n
物理复制操作。然而,真正的问题在于,对于s
我们追加的每个副本,我们的结果都会变长。这意味着结果中的每个连续长度检查也strcat()
越来越长。如果我们现在以“我们必须扫描或复印的次数”作为比较的基础来比较这两种解决方案,我们可以看到两种解决方案的不同之处在哪里。s
对于n
string 的副本s
,基本解决方案执行如下:
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 |
从表中可以看出,由于内置的长度检查,基本解决方案对我们的字符串执行 ( n^2 + n)/2 次strcat()
扫描,而优化解决方案总是执行(n + 1)次扫描。这就是为什么基本解决方案(以及依赖 的所有其他解决方案strcat()
)在O(n^2)中执行,而优化解决方案在O(n)中执行。
O( n ) 与 O( n^2 ) 相比如何?
当使用大字符串时,运行时间会产生巨大的差异。例如,让我们以一个s
1MB 的字符串为例,我们希望创建 1,000 个 (== 1GB) 的副本。如果我们有一个 1GHz CPU 可以扫描或复制 1 个字节/时钟周期,那么s
将生成 1,000 个副本,如下所示:
注意: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 )之间运行时间的差异。