让我从一开始就很清楚,这不是骗子,我将解释如何做。
因此,我要求自己编写一个模仿strcpy
但有两个条件的函数:
该函数应返回一个指向新复制的字符串的指针。因此,这是到目前为止我尝试过的:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char * my_strcpy(char *original);
int main(void) {
char *string = my_strcpy("alpine");
printf("string = <%s>\n", string);
return 0;
}
char * my_strcpy(char *original){
char *string = (char *)malloc(10);
if(*original == '\0') {
return string;
}
*string++ = *original;
my_strcpy(original + 1);
}
问题在某种程度上很明显,每次调用string
时malloc
就会被my_strcpy()
编辑。我能想到的解决方案之一是仅在第一次调用函数时为string
分配内存。由于只允许我使用1个参数,因此我唯一想到的就是检查调用堆栈,但是我不知道是否允许这样做,而且感觉确实很欺骗。
这个问题有逻辑解决方案吗?
答案 0 :(得分:3)
您将其写为尾递归,但我认为不使函数不可重入,您唯一的选择就是使函数头部递归,并在递归调用的返回值上反复调用realloc以扩展它,然后加一个字符。这与仅调用strlen进行分配具有相同的问题:在每个递归调用中,它在输入字符串的长度上执行线性操作,结果是隐式n平方算法(0.5 * n *(n + 1) ))。您可以通过以下方法来改进它:使摊销时间的复杂性更好,将字符串扩展一个因子,然后仅在现有缓冲区已满时才增大字符串,但这仍然不是很好。
有一个原因,您将不使用递归执行此任务(您可能知道):堆栈深度将等于输入字符串的长度,整个堆栈帧被压入,并且复制的每个字符的调用指令很多高架。即使这样,如果您真的要递归地进行操作,也不会使用单个参数来递归地进行处理:您将使一个单参数函数声明一些局部变量,并使用多个参数来调用递归函数。
即使有了realloc技巧,也很难或无法在计算原始字符时对它们进行计数,以便可以适当地调用realloc,并记住其他stdlib“ str *”函数已超出限制,因为它们会可能会使您的整个函数为n平方,这是我认为我们试图避免的。
可以使用一些丑陋的技巧,例如验证字符串是否与指针一样长,并使用memcpy用指针替换前几个字符,这使得递归的基本情况更加复杂,但是,嗯,。
答案 1 :(得分:0)
您没有说我们不能使用strcat。 因此,通过使用递归除掉最后一个字符再将其重新添加,该命令是合乎逻辑的(尽管有些无用)的答案。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char * my_strcpy(char *original);
int main(void) {
char *string = my_strcpy("alpine");
printf("string = <%s>\n", string);
return 0;
}
char * my_strcpy(char *original){
if(*original == '\0') {
return original;
}
int len = strlen(original);
char *string = (char *)malloc(len+1);
char *result = (char *)malloc(len+1);
string[0] = result[0] = '\0';
strcat (string, original);
len--;
char store[2] = {string[len] , '\0'}; // save last char
string[len] = '\0'; // cut it off
strcat (result, my_strcpy(string));
strcat (result, store); // add it back
return result;
}
答案 2 :(得分:0)
递归是一种用于分析问题的技术。也就是说,从问题开始,然后考虑解决方案的递归结构。您不是从递归结构开始的,而是试图将您的问题随意地拖入其中。
换句话说,实践递归分析是很好的,但是您自己设定的任务-迫使解决方案具有单参数函数的形式-并非做到这一点。如果您开始考虑全局变量或静态变量,或者通过进入调用堆栈来提取运行时上下文,则可以很好地暗示尚未找到合适的递归分析。
这并不是说没有完美的递归解决方案来解决您的问题。有一个,但是在开始之前,我们可能想要抽象出问题的详细信息以提供一些动力。
很显然,如果内存中已经有一个连续的数据结构,那么制作一个连续的副本就没有挑战性了。如果我们不知道它有多大,则可以进行两次遍历:一次遍历以找到其大小,然后我们可以分配所需的内存,而另一遍遍进行复制。这些任务都是简单的循环,这是递归的一种形式。
递归解决方案的本质是考虑如何从一个问题过渡到一个(稍微)简单或更小的问题。或者,更常见的是少数较小或更简单的问题。
这是最经典的递归问题之一的本质:对数字序列进行排序。基本结构:将序列分为两个大致相等的部分;对每个部分进行排序(递归步骤),然后将结果放回一起,以便对组合进行排序。基本轮廓至少具有两个有趣(且非常不同)的表现形式:
通过将交替元素放在交替的部分中或者将前半部分放在一个部分中,然后将其余部分放在另一部分中,将该序列任意地划分为两个几乎相等的部分。 (如果我们事先不知道序列有多大,第一个将很好地工作。)为了将排序的部分放在一起,我们必须对它们进行交织(“合并”)。 (这是mergesort)。
通过估计中间值并将所有较小的值放入一个部分,将所有较大的值放入另一部分,将序列分为两个范围。为了将排序的部分放在一起,我们只是将它们连接在一起。 (这是快速排序。)
在这两种情况下,我们还需要利用以下事实:对单个元素序列进行(简单)排序,因此无需进行其他处理。如果我们经常将序列分为两个部分,并确保两个部分都不为空,那么我们最终必须达到包含一个元素的部分。 (如果我们设法准确地进行划分,那将很快发生。)
使用单链接列表实现这两种策略很有趣,因此长度的确不容易知道。两者都可以通过这种方式实现,并且实现揭示了有关排序性质的一些重要信息。
但是让我们回到手头的简单问题上,将序列复制到新分配的连续数组中。为了使问题更有趣,我们不会假定该序列已经连续存储,也不会假定它可以遍历两次。
首先,我们需要找到序列的长度,我们可以通过观察一个空序列的长度为零,而其他任何序列都比从第一个元素开始的子序列多一个元素(“尾巴”的顺序。)
Length(seq):
If seq is empty, return 0.
Else, return 1 + Length(Tail(seq))
现在,假设我们已经为副本分配了存储空间。现在,我们可以观察到空序列已完全复制,然后通过将第一个元素放入分配的存储区,然后将序列的尾部从第二个位置开始插入存储区中,即可复制任何其他序列:此过程在逻辑上有两个参数)
Copy(destination, seq):
If seq is not empty:
Put Head(seq) into the location destination
Call Copy (destination+1, Tail(seq))
但是我们不能仅仅将这两个过程放在一起,因为那样会遍历两次序列,这是我们说不能做的。因此,我们需要以某种方式嵌套这些算法。
要做到这一点,我们必须首先将累加的长度向下递归,以便在知道对象的大小时可以使用它来分配存储空间。然后,在返回的过程中,我们需要复制在返回过程中计算的元素:
Copy(seq, length):
If seq is not empty:
Set item to its first element (that is, Head(seq))
Set destination to Copy(Tail(seq), length + 1)
Store item at location destination - 1
Return destination - 1
Otherwise: (seq is empty)
Set destination to Allocate(length)
# (see important note below)
Return destination + length
要正确启动递归,我们需要传入0作为初始长度。强制用户插入“魔术数字”是一种不好的风格,因此我们通常使用单参数驱动程序包装该函数:
Strdup(seq):
Return Copy (seq, 0)
重要说明:如果是使用C语言使用字符串编写的,则需要NUL终止副本。这意味着分配length+1
字节而不是length
,然后在destination+length
上存储0。