【问题标题】:Invert a string: Recursion vs iteration in javascript反转字符串:javascript中的递归与迭代
【发布时间】:2011-06-10 17:39:14
【问题描述】:

一个月前,我接受了一些谷歌 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),因为运行时间线性地取决于输入。 - 沉默 - 然后,他问我,如果你通过迭代来实现,在运行时间方面有什么区别?

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

非常感谢和问候!

【问题讨论】:

  • 我不确定,但递归中可能存在调用堆栈生成开销,这不会是循环中的。
  • 顺便说一句,我会存储 s.length 而不是访问它 3 次,但也许不需要优化......(对不起,我总是过早地优化,这是我的许多错误之一)
  • @Martin:调用堆栈生成应该是非常很小的开销。无论如何,这里的代码递归 n 次,它的计算复杂度与迭代 n 次的循环相同。
  • @Martin @Juliet:谢谢,所以.. 你认为(除了可以存储的长度)计算复杂度不会从递归到迭代吗?
  • 我绝对不是编译器/解释器专家,所以我只能根据我自 1997 年以来的 javascript 编码经验(服务器端、netscape faststrack 服务器)给你一些思考,不要问;)。如果您从一开始就知道要经历的迭代次数,迭代总是比递归快。在一个实际需要投入生产的项目中,我自己永远不会通过递归来实现字符串反转。也就是说,我发现它是一个优雅的解决方案:)

标签: javascript recursion iteration big-o


【解决方案1】:

您的解决方案在我看来是 O(n²)。对substring 的调用很可能是 O(n) — 一个典型的实现将为新字符串分配空间,然后复制子字符串。 [但请参阅 cmets。] 字符串连接 + 也可能是 O(n)。 length 甚至可能是 O(n),但我认为这不太可能。


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

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

【讨论】:

  • 真的吗?我不太了解 javascript,但在我知道的大多数语言中,子字符串是 O(1),共享原始字符串的存储(因为字符串是不可变的,或者子字符串是使用写时复制实现的)。
  • 这个问题没有说明正在使用哪个 JavaScript 实现,因此我需要注意。 (我曾经实现过 JavaScript,而我的 substring 是 O(n),所以至少有一个 JS 实现是这样的!)但即使你对 substring 的看法是正确的,+ 似乎也可能是 O( n).
  • @sepp2k:O(1) 子串的问题在于它确实搞砸了垃圾收集。如果我分配一个多兆字节的字符串,从中间拉出 5 个字符,然后让原始字符串超出范围,它仍然不符合收集条件,因为微小的子字符串仍在浮动。
  • @Gareth:是的,+ 几乎可以肯定是 O(n),这是真的。
  • @sepp2k:在您的评论提示下,我做了一些研究,据我所知,substring implementation in V8 按照我的描述进行分配和复制。
【解决方案2】:

附带说明,要使用允许编译器将递归实现为循环的尾递归,您不能将状态留在堆栈中。要解决此问题,您可以将“状态”作为附加参数传递给您的函数:

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

【讨论】:

  • 我认为大多数(如果有的话)JavaScript 引擎不会执行尾调用优化
  • 没错,我只是提到它,因为OP在他的问题中提出了优化的可能性
  • 你是对的@BrokenGlass。该示例应该优化所有“可优化”的东西:D ..我想他问我这种事情只是为了知道我是否能够对 opt 做任何事情。顺便说一句,在这种情况下,你认为递归不是“转换的” "进入迭代?
  • 在您给出的示例中,即使在理论上也无法优化,因为您 return xxx + invert(yyy) 所以编译器必须将 xxx 保留在堆栈上,递归计算 invert(yyy) 然后展开堆栈并执行添加。这就是为什么最好做一个return invert(yyy, xxx) 这样堆栈上就没有状态了。这称为尾递归,可以优化为循环(但不适用于 Javascript)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-05-18
  • 2020-08-09
  • 1970-01-01
  • 1970-01-01
  • 2012-09-24
  • 2020-06-07
  • 1970-01-01
相关资源
最近更新 更多