【问题标题】:Concatenate null-terminated strings recursively递归连接以空字符结尾的字符串
【发布时间】:2010-12-23 03:52:05
【问题描述】:

我正在发明需要分析 C++ 代码的面试问题,这些代码使用指针和递归来做一些简单的事情。我尝试以递归方式写strcat():

size_t mystrlen( const char* str )
{
    if( *str == 0 ) {
        return 0;
    }
    return 1 + mystrlen( str + 1 );
}

void mystrcpy( char* to, const char* from )
{
    if( ( *to = *from ) == 0 ) {
        return;
    }
    mystrcpy( to + 1, from + 1 );
}

void mystrcat( char* to, const char* from )
{
    mystrcpy( to + mystrlen( to ), from );
}

我不喜欢这里的是我有三个函数,而我的 strcat() 不是很递归 - 它只调用其他两个函数一次。有没有办法以某种方式重写它以减少函数的数量,增加递归的使用,但又不牺牲代码的简洁性?

【问题讨论】:

  • 我认为这没有多大意义。也许你最好找到一个可以用递归解决的真正问题(很简单)并去做。此外,正确编写递归通常比阅读递归更难。树及其上的任何操作通常都是很好的候选者。在一次采访中,有人问我如何实现存储整数的树(结构),然后进行一些操作,例如“返回所有值的总和”、“返回从根开始计数的 N 级所有值的总和”。 ..
  • 您的mstrlen() 和mstrcpy() 将导致...堆栈溢出...如果提供任何可观大小的数据...
  • (当然,除非你得到了尾递归优化的祝福……)
  • 也许这个问题的想法是,任何在问题结束时身体没有不适的候选人,都没有 C 编程的实际经验;-)
  • 如果我被问到这样的问题,我会跑路的:o)

标签: c++ c string recursion


【解决方案1】:

怎么样:

void mystrcat( char* to, const char* from )
{
    if (*to == '\0')
        mystrcpy( to, from );
    else
        mystrcat( to + 1, from );     
 }

【讨论】:

  • 我不太喜欢*to == NULL这句话,即使它有效。取消引用char* 会导致一个可以是0 或nil 的字符(如您所愿),但NULL 是一个指针。我知道在 C++ 中它实际上被定义为0,但有点说“这是一个无处指向的指针”。
  • 不过,除此之外还是很不错的。
  • @dribeas - 你完全正确,字符不应该与 NULL 指针进行比较,而应该与 NUL 字符('\0')进行比较。如果我费心编译这个,gcc 会给我一个适当的警告。无论如何,我已经更新了我的示例。
【解决方案2】:

我想你已经完成了。您有两个可爱的递归函数,以及对它们的单行调用。

我只能想到一种骇人听闻的方式,甚至可以在一个函数中做到这一点:

void
mystrcat(char* to, const char* from, bool copy_now)
{
    if (!copy_now && *to)
    {
        mystrcat(to+1, from, false);
        return;
    }

    *to = *from;
    if (*to)
        mystrcat(to+1, from+1, true);
}

如果您使用 C++ 并且您将 copy_now 设为默认值为 false 的可选参数,那几乎不会很糟糕。然后调用者可以假装它没有那个额外的状态。

但是,如果问题很难让您在 StackOverflow 上寻求帮助,那么它就很难成为一个合理的面试问题。您可以要求它只是为了试图让人们跳出框框思考,但不要让未能得到好的答案自动失败。

我建议添加另一个可爱的递归问题:求二叉树的深度。

给定以下结构:

typedef struct s_node
{
    struct s_node *pLeft;
    struct s_node *pRight;
} NODE;

写一个函数

int tree_depth(NODE *pNode)

返回树的最大深度。实际上,我曾经在一次面试中被问过这个问题,我认为这正是面试的合适难度。您不想雇用的人会遇到麻烦,而您可能想雇用的人可能会喜欢它。

编辑:我测试了这个函数,发现它有一个错误。此版本已修复错误。

【讨论】:

    【解决方案3】:

    您可以在一个函数中编写:

    // pre: to is a string that is zero-initiallized (all chars are '\0')
    void mystrcat( char* to, const char* from )
    {
       if ( *to ) mystrcat( to+1, from );
       else {
          if (*to = *from) mystrcat( to+1, from+1 );
       }
    }
    

    请注意,它是故意丑陋的,它用于两个分支,它在第一个分支中推进to 指针,并在另一个分支中复制值。 if 条件也故意丑陋,因为通常您会将条件中的单个 = 标记为拼写错误,但坚持认为它已启动、运行和测试,以便他们必须自己弄清楚含义。

    您可能会考虑的另一件事是添加一个返回语句,例如:

    char* mystrcat2( char* to, const char* from )
    {
       if ( *to ) return mystrcat2( to+1, from );
       else {
          if ( *to = *from ) return mystrcat2( to+1, from+1 );
          else return to;
       }
    }
    

    现在受访者的第二个任务是解释函数在两个字符串上调用时的结果,你可以讨论这个strcat版本的效率与原始版本相比如何,无论是在单字符串连接方面还是在多个连接(查看 Joel Spolsky 的 this article,其中讨论了 Scheimel 绘制算法)。您可以通过strcat 和mystrcat 来要求实现这个功能:

    // concatenates a set of strings into a buffer
    //
    // pre:  to has memory enough to hold all the contents
    //       from[i] is a valid C string for all i smaller than n
    void mymultiplecat( char* to, const char* from[], int n )
    {
       for ( int i = 0; i < n; ++i ) {
          to = mystrcat( to, from[i] );
       }
    }
    

    【讨论】:

    • 我不明白这是如何工作的。在找到以to 结尾的nul 字符之前,它会很好地工作,但是一旦它从*from 复制一个字符,它就会开始尝试再次找到to 的结尾。它需要一个额外的变量来知道它处于哪个状态,找到to 的结尾或复制。我的回答与此基本相同,但使用bool 第三个参数来提供额外的状态。
    • @dribeas: 你的版本只有在 *to in 清零到结束时才能工作
    • 没错,我将其添加为前提条件。正如@steveha 指出的那样,该算法可以移动到两个函数中,此时我将提供一个调用三个参数实现的两个参数接口。无论如何,我将保留它,因为它是添加的 return 声明和潜在的讨论。
    • 好的,它适用于该前提条件。我只是总是假设字符串末尾的任何内存都充满了随机垃圾,因为它通常是。
    【解决方案4】:

    好吧,我真的不明白尝试使 strcat() “尽可能递归”的意义,即使它只是用于面试问题。 strcpy() 只是一个 strlen() + strcat() 并且实现应该反映这一点。如果应聘者提供了您所勾画的解决方案,我会非常高兴 - 他/她表明他/她了解 1. 递归和 2. 使用子例程来实现更复杂的功能。

    【讨论】:

    • 我同意。 strlen() 和 strcpy() 都适用于递归解决方案,但不适用于 strcat()。
    【解决方案5】:

    这是我的例子(优点是它只有一个递归调用):

    char * mystrcat(char *dest, const char *src){
        if(*dest == 0){
            if(*src == 0)   // end of recursion cond
                return dest;
            *dest = *src++; // actual copy
            dest[1]=0;      // zero out dest buf
        }
        mystrcat(dest+1,src); // advance one char
        return dest;
    }
    

    这里是粗略的测试代码:

    main(){
      char c[]={'a',0,'b','c',0};
      //char c[]={0,'b','c',0};
      mystrcat(c,"xy");
      //mystrcat(c,"");
      puts(c);
    }
    

    【讨论】:

    • 我想出了这样的第二个答案。你的更整洁,因为它在一个方向上做所有事情;我的直到找到from 字符串的结尾,然后在递归调用返回时复制字符。 +1。
    【解决方案6】:

    我想到了另一个答案!

    void
    mystrcat(char* to, const char* from)
    {
        if (*to)
        {
            // keep looking for end of to
            mystrcat(to+1, from);
            return;
        }
    
        if (*from)
        {
            // make sure we get past first if
            to[1] = '\0';
            // call recursively until end of from found
            mystrcat(to+1, from+1);
            // after call, copy chars on way out
            *to = *from;
        }
    }
    

    没有额外的参数来保持状态,它仍然是 O(n)。

    不过,我不确定自己能否在工作面试的压力下提出这个想法。

    【讨论】:

    • 我认为问题更多是关于编写代码,然后询问受访者它实际上做了什么。如果我要求某人实现strcat,并且他们使用这些选项中的任何一个递归地执行此操作,那将说明候选人的很多(并且没有什么好处)(当然,除非背景是 lisp)
    【解决方案7】:

    有一次我有一个面试问题,我必须编写一个简单的递归 BSP 树遍历,它是一个完整程序的实际空白函数。

    我建议,与发明一个在 C++ 中没有惯用递归解决的问题相比,类似的方法是衡量某人对递归的掌握程度的更好方法。此外,当您运行程序并且他们观察到他们的代码完美运行时,对于入门级申请者来说,这是令人兴奋的。

    如果您坚持使用 strcat,我认为如果任务是递归连接从链表中获取的任意数量的字符串可能会更有趣。即使数据复制和字符串长度操作不是递归的,链表的遍历也可能是,而且我认为它会更有趣,因为您实际上可以在向下和向上的过程中进行工作。

    【讨论】:

      【解决方案8】:

      你想要递归和简洁?这是我认为兼具这两种品质的东西,而无需深入研究高尔夫球杂草(这并不是说它不丑 - 确实如此):

      char* myStrcat( char* to, char const* from)
      {
          if (!*from) return to;
      
          if (!*to) *to++ = *from++, *to-- = '\0';
      
          return myStrcat( to+1, from) - 1;
      }
      

      请注意,此版本增加了返回目标指针的复杂性(如标准strcat())。

      这是一个更易读的版本,它使用较少的“伪聪明”,如逗号运算符,并在递增后递减以恢复 to 指针(出于某种原因,steveha 的评论促使我这样做)。再看这里的各种答案几乎等同于catwalk's answer:

      char* myStrcat( char* to, char const* from)
      {
          if (!*from) return to;  // terminal condition, no more chars to concatenate
      
          if (!*to) {
              // since we're at the end of the 'to' string, 
              //    append char from 'from'
              //    and 'consume' it from `from`
              *to = *from++;
              *(to+1) = '\0';
          }
      
          return myStrcat( to+1, from) - 1;
      }
      

      但是,请不要将我的名字与任何此代码相关联(我声称它是从其他人那里窃取的)。

      【讨论】:

      • 不要做*to--,只要做*to,然后你就不用在递归调用中传递to+1了。或者,您可以按照我的方式进行操作,只需说*to = *from;,然后是to[1] = '\0';,然后在递归调用中将+1 放在to= 和from 上。
      • @steveha - 肯定有很多选择。您只需要注意,如果您不执行*to-- 并传入to 而不是to+1,则在它不指向终止零字符的情况下增加to(即,第二个if 需要else)。从可读性的角度来看,您使用索引的替代方案更可取。但请记住 - 我没有声明此代码的作者身份(无论如何它都不是良好编码风格的示例)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-06-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-07
      相关资源
      最近更新 更多