在C中递归练习

时间:2012-07-01 11:31:08

标签: c string recursion

您如何解决涉及递归的以下问题?

使用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;
}

5 个答案:

答案 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的每个副本附加到结果。基本解决方案的行为如下:

  1. 为任意数量的重复返回零长度字符串 空字符串。
  2. 返回非空的零或负迭代的零长度字符串 字符串。
  3. 返回非零正数的非零长度字符串 在非空字符串上重复。
  4. 如果您基于上述功能创建程序,请使用以下语句:

    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次来将sstrcat()个副本附加到结果中。这就是问题所在,因为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()返回的字符串,以避免内存泄漏。