您如何解决涉及递归的以下问题?
使用prototype char *repeat(char *s, int n)
实现一个函数,以便它创建并返回一个字符串,该字符串由输入字符串s的 n 重复组成。例如:如果输入为“Hello”且为3,则输出为“HelloHelloHello”。仅使用递归构造。
我的解决方案在我看来非常难看,我正在寻找更清洁的东西。这是我的代码:
char *repeat(char *s, int n) {
if(n==0) {
char *ris = malloc(1);
ris[0] = '\0';
return ris;
}
int a = strlen(s);
char *ris = malloc(n*a+1);
char *ris_pre = repeat(s,n-1);
strcpy(ris,ris_pre);
strcpy(ris+(n-1)*a,s);
free(ris_pre);
return ris;
}
答案 0 :(得分:9)
更加整洁和优雅的解决方案(我称之为基本解决方案)如下:
基本解决方案
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
副本的位置,以便我们将strcat()
替换为strcpy()
每次s
都会strcat()
添加到结果中。这提供了O( n )的实际运行时间,而不是O( n ^ 2 )。
基本解决方案和优化解决方案之间的区别是什么?
所有其他解决方案在字符串n
上使用s
至少n
次来将s
个strcat()
个副本附加到结果中。这就是问题所在,因为strcat()
的实施隐藏了低效率。在内部,strcat = strlen + strcpy
可以被认为是:
n
即,在追加时,首先必须找到你之前附加到的字符串的结尾,你可以自己追加。这种隐藏的开销意味着,实际上,创建字符串的n
个副本需要n
长度检查和s
物理复制操作。但是,真正的问题在于,对于我们附加的strcat()
的每个副本,我们的结果会变得更长。这意味着结果中s
内的每个连续长度检查也会变得更长。如果我们现在使用" 比较两个解决方案,我们必须扫描或复制n
"作为比较的基础,我们可以看出两种解决方案的区别所在。
对于字符串s
的{{1}}个副本,基本解决方案执行如下:
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 |
而Optimized Solution的表现如下:
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)中执行< / em>的
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(小1千倍),并制作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)
这是一个真正令人震惊的差异。优化的解决方案与之前同时执行,因为写入的数据总量相同。但是,Basic Solution会停止半天来构建结果。这是O( n )和O( n ^ 2 )之间运行时间的差异。
答案 1 :(得分:3)
尝试这种只分配字符串一次的方法:
char *repeat(char *s, int n) {
int srcLength = strlen(s);
int destLength = srcLength * n + 1;
char *result = malloc(destLength);
result[0] = '\0'; // This is for strcat calls to work properly
return repeatInternal(s, result, n);
}
char *repeatInternal(char *s, char *result, int n) {
if(n==0) {
return result;
}
strcat(s, result);
return repeat(result, s, n-1);
}
第二种重复方法只能由第一种方法使用。 (第一个是你的原型方法)
注意:我没有编译/测试它,但这应该有效。
答案 2 :(得分:2)
这是一个:
char *repeat (char *str, int n)
{
char *ret_str, *new_str;
if (n == 0)
{
ret_str = strdup ("");
return ret_str;
}
ret_str = repeat (str, n-1);
new_str = malloc (sizeof (char) * strlen (str) * (n + 1));
new_str[0] = '\0';
strcpy (new_str, ret_str);
strcat (new_str, str);
free (ret_str);
return new_str;
}
我们可以通过realloc ()
char *repeat (char *str, int n)
{
char *ret_str;
if (n == 0)
{
ret_str = strdup ("");
return ret_str;
}
ret_str = repeat (str, n-1);
ret_str = realloc (ret_str, sizeof (char) * strlen (str) * (n + 1));
strcat (ret_str, str);
return ret_str;
}
编辑1
好的,这个更紧凑。
char *repeat (char *str, int n)
{
static char *ret_str;
static int n_top = -1;
if (n >= n_top)
ret_str = calloc (sizeof (char), strlen (str) * n + 1);
if (n <= 0)
return ret_str;
n_top = n;
return strcat (repeat (str, n-1), str);
}
我们使用静态缓冲区来保存最终字符串,因此在所有递归级别中都使用了一个缓冲区。
static int n_top
保存递归调用的前一个值n
的值。这由-1
初始化以处理使用n = 0
调用的情况,因此它返回一个空字符串(并且calloc
用于初始化为0)。在第一次递归调用时,值为-1
,因此仅在顶级n > n_top
为真(因为n
总是递减),并且在这种情况下整个缓冲区被分配{{1 }}。另外我们找到了底部条件,即ret_str
变为0.此时n
我们将预先分配的静态缓冲区n = 0
的地址返回给父级调用者递归树。然后,ret_str
附加的每个递归级别使用此单个缓冲区,并将其移交给上一级,直到达到str
。
编辑2
更紧凑,但丑陋
main
如果您使用char *repeat (char *str, int n)
{
static int n_top;
n_top = (n_top == 0)? n: n_top;
return (n <= 0)?(n=n_top,n_top=0,calloc (sizeof (char), strlen (str) * n + 1)):strcat (repeat (str, n-1), str);
}
调用,则最后一个紧凑代码会出现问题。这种实现克服了这个问题,而且它也更加紧凑,只使用一个功能。
请注意,有一个丑陋的repeat (str, n); repeat (str, 0);
。在此我们确保在回滚时,我们使用(n=n_top,n_top=0,calloc (sizeof (char), strlen (str) * n + 1))
的值来分配内存,然后将n_top
重置为n_top
,以便该函数将0
设置为n_top
在来自0
或其他主调用者的下一个调用中(不是递归的)。这可以用更易读的方式完成,但这看起来很酷。我建议坚持使用更易读的。
编辑3
狂人版
这克服了重复main ()
次来电。 strlen ()
只被调用一次,然后使用当前深度中字符串长度的值以及strlen ()
的值来查找表示最终结束的n
值返回的字符串(其地址不存储在任何中间变量中,只返回并传递)。将字符串传递给offset
时,我们添加偏移量并将源内存位置提供给memcpy
,方法是将memcpy
添加到紧接下一个深度的返回答案字符串中。这实际上在字符串结束后立即提供offset
位置,之后memcpy
复制长度为memcpy
的内容str
。请注意str_len
将返回传递的目标地址,即此深度的答案字符串结束地址,但我们需要实际开始,这是通过从此返回值返回memcpy
来实现的。 ,这就是为什么在返回之前减去offset
的原因。
请注意,仍然使用单一功能:D
offset
一些注意事项:
我们可能已经完成char *repeat (char *str, int n)
{
static int n_top, str_len;
int offset = 0;
(n_top == 0)?(n_top = n,str_len = strlen (str)):(offset = str_len * (n_top-n));
return (n <= 0)?(n=n_top,n_top=0,malloc (str_len * n + 1)):(memcpy (repeat (str, n-1) + offset, str, str_len) - offset);
}
,在第一个深度中,offset = str_len * (n-1)
将被复制到偏移0中,从后续的递归深度,它会将字符串从反向复制到答案字符串
执行str
时,我们会告诉它复制memcpy
字节,但不包括n
。但是当我们使用\0
为终止''\ 0'字符的空间分配最终目标内存时,它被初始化为0.因此最后的字符串将被'\ 0'终止。
sizeof(char)始终为1
要使其看起来更紧凑和神秘,请删除calloc
计算并直接计算上一个offset
表达式中的偏移量。
请勿在现实生活中使用此代码。
答案 3 :(得分:0)
这是一个需要更多代码的解决方案,但它在O(log n)时间内运行而不是O(n):
// Return a string containing 'n' copies of 's'
char *repeat(int n, char *s) {
return concat((n-1) * strlen(s), strdup(s));
}
// Append 'charsToAdd' characters from 's' to 's', charsToAdd >= 0
char *concat(int charsToAdd, char *s) {
int oldLen = strlen(s);
if (charsToAdd <= n) { // Copy only part of the original string.
char *longerString = malloc((oldLen + charsToAdd + 1) * sizeof(char));
strcpy(longerString, s);
strncat(longerString, s, charsToAdd);
return longerString;
} else { // Duplicate s and recurse.
char *longerString = malloc((2 * oldLen + 1) * sizeof(char));
strcpy(longerString, s);
strcat(longerString, s);
free(s); // Free the old string; the recusion will allocate a new one.
return concat(charsToAdd - oldLen, longerString);
}
}
答案 4 :(得分:0)
可能的解决方案:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *repeat(char *s, int n)
{
static char *sret=NULL;
static int isnew=1;
if (!s || !s[0])
{
if (sret) { free(sret); sret=NULL; }
return "";
}
if (n<=0) return "";
if (isnew)
{
int nbuf = strlen(s)*n + 1;
sret = (char*)realloc(sret, nbuf);
memset(sret, 0, nbuf);
isnew=0;
}
strcat(sret,s);
repeat(s, n-1);
isnew = 1;
return sret;
}
int main()
{
char *s = repeat("Hello",50);
printf("%s\n", s);
s = repeat("Bye",50);
printf("%s\n", s);
repeat(NULL,0); /* this free's the static buffer in repeat() */
s = repeat("so long and farewell",50);
printf("%s\n", s);
return 0;
}
[edit]
aps2012解决方案的变体,它使用单个函数,但带有静态int:
char *repeat(char *s, int n)
{
static int t=0;
return (n > 0)
? (t += strlen(s),strcat(repeat(s, n - 1), s))
: strcpy(malloc(t + 1), "");
}
调用者必须free()
返回的字符串,以避免内存泄漏。