【问题标题】:Avoid stackoverflow when recursively dismantling a string递归拆解字符串时避免stackoverflow
【发布时间】:2019-12-15 11:25:15
【问题描述】:

我正在为advent of code 2018剧透警告)的问题寻找解决方案,我需要一个函数,该函数接受一个字符串(或一个char list)并删除每一对他们反应时的字符。该练习描述了“聚合物”中的两个字符或“元素”,当它们是相同的字母但仅大小写不同时会发生反应;所以从AbBc 开始会留下Ac。请记住,在一个反应​​之后,两个字符可能会彼此相邻,而不是以前,并引起新的反应。

我想我可以通过使用只处理前两个字符并递归调用自身的递归函数来解决这个问题,但由于输入字符串很大,这会导致stackoverflow exception

let rec react polymer =
    match polymer with
    | [] -> []
    | [x] -> [x]
    | head::tail ->
        let left = head
        let right = List.head tail
        let rest =  List.tail tail
        // 'reacts' takes two chars and
        // returns 'true' when they react
        match reacts left right with
        // when reacts we go further with
        // the rest as these two chars are
        // obliterated
        | true -> react rest
        // no reaction means the left char
        // remains intact and the right one 
        // could react with the first char 
        // of the rest
        | false -> [left] @ react tail

然后,只是试图解决这个练习以获得单元测试的正确答案,我试图强制执行它,但这很快就变得一团糟,现在我有点卡住了。我正在自学f#,所以欢迎任何指点。谁能以功能的方式解决这个问题?

【问题讨论】:

  • 尾递归是避免 stackoverflow 的一种方法。另外我认为你可以像这样| left::right::tail -> 左右模式匹配

标签: recursion f# stack-overflow tail-recursion


【解决方案1】:

您可以通过重写函数以使用尾递归来避免堆栈溢出,这只是意味着递归调用应该是最后执行的操作。

当您执行[left] @ react tail 时,您首先进行递归调用,然后将[left] 附加到结果中。这意味着它必须在执行递归调用时保留当前函数上下文(称为堆栈帧),并且如果递归调用也将堆栈帧加起来,直到出现堆栈溢出。但是如果在当前函数上下文中没有更多工作要做,堆栈帧可以被释放(或重用),因此不会发生堆栈溢出。

您可以通过添加另一个函数参数使其尾递归,通常称为acc,因为它“累积”值。我们没有将left 添加到递归调用的返回值中,而是将其添加到累加器中并传递它。然后当我们耗尽输入时,我们返回累加器而不是空列表。

我还冒昧地附加了[left] @ ...,作为缺点left::...,因为后者比前者效率更高。我还将leftrightrest 移到了该模式中,因为这样更整洁、更安全。您通常应该避免使用List.headList.tail,因为它们在空列表中失败并且是等待发生的错误。

let rec react acc polymer =
    match polymer with
    | [] -> acc
    | [x] -> x::acc
    | left::right::rest ->
        match reacts left right with
        | true -> react acc rest
        | false -> react (left::acc) (right::rest)

您也可以使用守卫代替嵌套的matches(实际上应该是if):

let rec react acc polymer =
    match polymer with
    | [] ->
        acc
    | [x] ->
        x::acc
    | left::right::rest when reacts left right ->
        react acc rest
    | left::rest ->
        react (left::acc) rest

【讨论】:

  • 感谢您花时间深入解释这一点并指出最佳实践。只有一个问题,当您说which should really have been an if anyway 时,这是否意味着当函数的结果是boolean 时,您应该只使用if/then/else
  • 虽然我不是 glennsl,但我有信心回答“是”。如果您看到match (some expression) with | true -> x | false -> y,那么写if (some expression) then x else y 几乎总是更好。匹配表达式更好的少数情况是布尔值不是您要匹配的 only 事物,例如match Int32.TryParse(str) with | true, value -> value + 5 | false, _ -> 0.
  • 确实,if 表达式读起来更好,因为形状对于布尔条件更可预测。如果您需要更多条件if ... else if ... else ...,它也会更好地链接。例外情况可能是您需要更像真值表的东西,我认为它作为match 读起来要好得多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-10
  • 2020-02-05
  • 2016-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多