反转字符串:javascript中的递归与迭代

反转字符串:javascript中的递归与迭代,javascript,recursion,iteration,big-o,Javascript,Recursion,Iteration,Big O,一个月前,我接受了一些谷歌PTO成员的采访。 其中一个问题是: 在js中递归反转字符串,并用大O表示法解释运行时间 这就是我的解决方案: function invert(s){ return (s.length > 1) ? s.charAt(s.length-1)+invert(s.substring(0,s.length-1)) : s; } 我觉得很简单 关于big-o符号,我很快回答了o(n),因为运行时间与输入成线性关系沉默——然后,他问我,如果通过迭代实现,在运行时间

一个月前,我接受了一些谷歌PTO成员的采访。 其中一个问题是: 在js中递归反转字符串,并用大O表示法解释运行时间

这就是我的解决方案:

function invert(s){
    return (s.length > 1) ? s.charAt(s.length-1)+invert(s.substring(0,s.length-1)) : s;
}
我觉得很简单

关于big-o符号,我很快回答了o(n),因为运行时间与输入成线性关系沉默——然后,他问我,如果通过迭代实现,在运行时间方面有什么区别

我回答说,有时编译器会将递归“翻译”成迭代(一些编程语言课程记忆),所以在这种情况下,迭代和递归没有区别。顺便说一句,由于我对这个问题没有任何反馈,面试官也没有回答“ok”或“nope”,我想知道你是否同意我的观点,或者你是否可以解释一下这两种实现是否存在差异

非常感谢和问候

我看你的解决方案是O(n²)。对
substring
的调用很可能是O(n)-一个典型的实现将为一个新字符串分配空间,然后复制整个子字符串。[但请参阅注释。]字符串串联
+
也可能是O(n)。甚至可能是
长度
为O(n),但我认为这是不太可能的


您提出了编译器可以将递归转换为迭代的想法。这是真的,但它很少在函数语言和Scheme之外实现;通常,应用的唯一转换是尾部递归消除。在您的代码中,递归不在尾部位置:在递归调用
invert
之后,您仍然需要计算
+
。所以尾部递归消除不适用于您的代码


这意味着
invert
的迭代版本必须以不同的方式实现。它可能具有相同或不同的复杂性,我们在看到它之前不能说。

另一方面,要使用尾部递归,允许编译器将递归实现为循环,堆栈上不能有状态。要解决此问题,可以将“状态”作为附加参数传递给函数:

function invert(sofar, s)
{
    return (s.length > 0) ? invert(s.charAt(s.length-1)+sofar, s.substring(0,s.length-1)) :  sofar;
}

我不确定,但递归中可能会有调用堆栈生成开销,这不会是循环中的。顺便说一句,我会存储s.length而不是访问它3次,但可能没有优化的必要。。。(很抱歉,我总是过早地进行优化,这是我的许多缺点之一)@Martin:调用堆栈生成的开销应该很小。在任何情况下,这里的代码都会递归n次,它的计算复杂度与循环重复n次的计算复杂度相同。@Martin@Juliet:谢谢,所以。。你认为(除了可以存储的长度外)计算复杂性不会从递归变为迭代吗?我不是一个长期的编译器/解释器专家,所以我能给你的只是基于我自1997年以来编写javascript的经验的一些思考(服务器端,netscape faststrack服务器,不问;)。如果你从一开始就知道迭代的数量,那么迭代总是比递归快。我自己永远不会在一个实际需要投入生产的项目中通过递归实现字符串反转。也就是说,我发现这是一个优雅的解决方案:)真的吗?我不太了解javascript,但在我所知道的大多数语言中,子字符串是O(1),共享原始字符串的存储(因为字符串是不可变的,或者子字符串是使用写时复制实现的)。问题没有说明使用了哪种javascript实现,因此我提出了警告。(我曾经实现过JavaScript,我的
子字符串
是O(n),所以至少有一个JS实现是这样!)但即使你对
子字符串
的看法是正确的,
+
似乎是O(n)。@sepp2k:O(1)子字符串的问题是它确实搞砸了垃圾收集。如果我分配一个多兆字节的字符串,从中间抽出5个字符,然后让原始字符串超出范围,它仍然不符合收集条件,因为微小的子字符串仍然在浮动。@Gareth:是的,
+
几乎肯定是O(n),这是真的。@sepp2k:根据您的评论,我做了一些研究,就我所知,分配和复制正如我所描述的。我不认为大多数(如果有的话)JavaScript引擎都会执行尾部调用优化。这是真的,我只是在OP在他的问题You's right@BrokenGlass中提出了优化的可能性之后才提过。这个例子应该优化所有“可优化”的东西:D.我想他问我这类问题只是想知道我是否能对opt做些什么。。顺便说一句,在这种情况下,你认为递归没有“转换”成迭代吗?在你给出的例子中,它不能被优化,即使是理论上,因为你
返回xxx+invert(yyy)
,所以编译器必须在堆栈上保持
xxx
,递归地计算
invert(yy)
,然后解开堆栈并进行加法。这就是为什么最好执行
返回反转(yyy,xxx)
,这样堆栈上就没有状态了。这被称为尾部递归,它可以优化为循环(但不适用于Javascript)