C 中的递归练习
Posted
技术标签:
【中文标题】C 中的递归练习【英文标题】:Exercise on recursion in C 【发布时间】:2012-07-02 03:40:51 【问题描述】:您将如何解决以下涉及递归的问题?
用原型实现一个函数 char *repeat(char *s, int n)
这样它就会创建并返回一个字符串,该字符串由 n 输入字符串s的重复组成。例如:如果输入是“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;
【问题讨论】:
【参考方案1】:一个更整洁优雅的解决方案(我称之为基本解决方案)如下:
基本解决方案
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) 之间运行时间的差异。
【讨论】:
【参考方案2】:尝试这种方法,只分配一次字符串:
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);
第二种重复方法只能由第一种使用。 (第一个是你的原型方法)
注意:我没有编译/测试它,但这应该可以工作。
【讨论】:
不是说“n 输入字符串的重复次数 s”吗?因此 0 次重复将是空字符串。 你应该分配srcLength * n + 1
字节。您当前的计算中没有空终止符的空间。
@interjay 没错,这也可以解决开头的烦人问题
C 没有函数重载,你应该重命名第二个函数。顺便说一句,您可以将第一个函数中的if
去掉以简化一点。
感谢更新。 (自从我用 c 编码以来已经很长时间了 :))【参考方案3】:
这是一个:
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
总是递减),在这种情况下,整个缓冲区被分配ret_str
。否则,我们找到底部条件,即n
变为0。此时,当n = 0
时,我们将预先分配的静态缓冲区ret_str
的地址返回给递归树中的父调用者。然后由str
附加的每一级递归使用这个单一缓冲区,并移交给上一级,直到到达main
。
编辑 2
更紧凑,但丑陋
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);
如果您将 call 与 repeat (str, n); repeat (str, 0);
一起使用,最后一个紧凑代码会出现问题。这个实现克服了这个问题,而且它更紧凑,也只使用一个函数。
请注意,有一个丑陋的(n=n_top,n_top=0,calloc (sizeof (char), strlen (str) * n + 1))
。这里我们确保在回滚时我们使用n_top
的值来分配内存,然后将n_top
重置为0
,以便该函数在来自main ()
的下一次调用中将n_top
设置为0
或其他主调用者(非递归)。这可以以更具可读性的方式完成,但这看起来很酷。我会建议坚持使用更易读的。
编辑 3
狂人版
这克服了重复的strlen ()
调用。 strlen ()
只被调用一次,然后字符串长度的值与当前深度的n
的值一起用于找到offset
值,该值表示返回的最终字符串的结尾(其地址不存储在任何中间变量中,只是返回并传递)。当将字符串传递给memcpy
时,我们添加偏移量并将源内存位置提供给memcpy
,方法是将offset
添加到从下一个深度返回的答案字符串中。这实际上提供了memcpy
紧跟字符串结尾的位置,之后memcpy
复制了长度为str_len
的东西str
。注意memcpy
会返回它传递的目的地址,也就是这个深度的应答字符串结束地址,但是我们需要实际的开始,这是通过从这个返回值返回offset
来实现的,也就是为什么在返回之前减去offset
。
注意这个仍然使用单一功能:D
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);
一些注意事项:
1234563 >在执行 memcpy
时,我们告诉它复制 n
字节,其中不包括 \0
。但是当我们使用calloc
来分配最终的目标内存以及终止 ''\0' 字符的空间时,它被初始化为 0。因此最终的字符串将以 '\0' 终止。
sizeof (char) 始终为 1
为了让它看起来更紧凑和神秘,删除offset
计算并直接计算最后一个return
表达式中的偏移量。
请勿在现实生活中使用此代码。
【讨论】:
带有静态缓冲区的版本只能工作一次:你不能这样做,例如repeat("Hello")
然后repeat("Cruel World")
- 它会崩溃!
@anatolyg:if (n > n_top)
条件。每次它位于顶层时,它都会分配新的缓冲区。
@anatolyg:它不会崩溃。并且缓冲区没有分配一次。为每个***调用分配一个缓冲区。
紧凑版本的建议:将ret_str
初始化为NULL,在if (n > n_top)
的情况下,如果不是NULL,则在再次分配空间之前释放它,或者重新分配空间并将memset 设置为0。
@phoxis 确实,我确实注意到您是唯一一个使用单个函数来执行此操作的人。这值得承认。【参考方案4】:
这是一个需要更多代码的解决方案,但它运行时间为 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);
【讨论】:
它需要O(log n)
堆栈上的额外空间,而不是时间
@anatolyg - 不知道你的意思。每次调用concat()
都会使字符串的长度加倍,例如,重复计数 64 只需要 6 次调用,而所有其他解决方案都需要 63 或 64 次。因此,此解决方案既节省时间又节省堆栈空间。
@AdamLiss 实际上,您的算法在 O(n) 中运行。您的执行时间需要 O(log n) 次递归,同意,但在这种情况下,n 指的是结果的长度(即结果中的字节数)。您的实现仍为 (n * strlen(s)
) 字节分配内存并复制 (n * strlen(s)
) 字节以完成。它不会通过仅复制和分配 (log(n) * strlen(s)
) 字节来创建 (n * strlen(s)
) 字节的结果,因此它不能称为 O(log n) 运行时间。
@aps2012 感谢您的澄清。我猜运行时间取决于程序大部分时间花在哪里:在strcat()
和strcpy()
(在这种情况下执行时间将是 O(n)),或者在malloc()
,以及执行时间如何malloc()
与正在分配的字节数有关。我倾向于认为它将花费大量时间复制数据,这确实会使其在 O(n) 中执行。
@AdamLiss 你的分析是对的,因为它大部分时间都花在strcat()
和strlen()
上。但是,我意识到这实际上使运行时间为 O(n^2),而不是 O(n)。原因是 strcat()
是在 result (每次迭代都在增长)而不是原始字符串上执行的。我曾经使用strcat()
,但现在我已经对其进行了优化以获得真正的 O(n)。看看吧。【参考方案5】:
一个可能的解决方案:
#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()
返回的字符串以避免内存泄漏。
【讨论】:
以上是关于C 中的递归练习的主要内容,如果未能解决你的问题,请参考以下文章