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 以将ns 副本附加到结果中。这就是问题所在,因为strcat() 的实现隐藏了低效率。在内部,strcat() 可以认为是:

strcat = strlen + strcpy

即,在追加时,您首先必须找到要追加到的字符串的结尾,之前您可以自己进行追加。这种隐藏的开销意味着,事实上,创建字符串的n 副本需要n 长度检查和n 物理复制操作。然而,真正的问题在于,对于我们附加的每个 s 副本,我们的结果会变得更长。这意味着在 resultstrcat() 内的每个连续长度检查也越来越长。如果我们现在以“我们必须扫描或复制 s 的次数”作为比较的基础来比较这两种解决方案,我们可以看到两种解决方案的不同之处在哪里。

对于字符串sn 副本,基本解决方案执行如下:

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 &gt; 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 &gt; n_top) 条件。每次它位于顶层时,它都会分配新的缓冲区。 @anatolyg:它不会崩溃。并且缓冲区没有分配一次。为每个***调用分配一个缓冲区。 紧凑版本的建议:将ret_str 初始化为NULL,在if (n &gt; 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 中的递归练习的主要内容,如果未能解决你的问题,请参考以下文章

数据--第21课-递归课后练习

使用交换和递归在 C 和 C++ 中的字符串反向性能

C语言 递归算法练习(阶乘,斐波那契..)

python练习-递归函数实现汉诺塔搬迁问题

python--第四天练习题

C 实战练习题目26 -递归法求阶乘