如何在 Scala 中在自引用树结构上创建尾递归合并方法(或者甚至可能)?

问题描述 投票:0回答:1

更新 2024.03.16: 提供了产生正确输出的代码,但仍然不是尾递归。


如何在 Scala 中在自引用树结构上创建尾递归

merge
方法(或者甚至可能)?

我已经研究这个问题好几天了。我读过有关接近它的文章,甚至是其他语言的文章。我什至将其提交给各种 AI(Bard、Copilot、AskCodi 等),它们返回的非功能代码仍然无法使用

@tailrec
注释进行编译。

我一定错过了将

merge
案例类中的
Node
方法转换为尾递归的简单思维跳跃。任何指导将不胜感激。

尤其是任何提供元认知方式来“思考”这种自下而上的解决方案风格构建的自我参照的东西(书籍、视频、文章等的链接)。

最后,我现在知道有些问题无法通过尾递归解决。是不是这个的情况呢?如果是这样,为什么?


更正代码:

这实现了所需的效果,但不是尾递归。

object Node {
  val Terminal: Node = Node(true, Map.empty)
}

final case class Node(
    isWord: Boolean
  , nodeByLetter: Map[Char, Node]
) {
  require(
    isWord || nodeByLetter.nonEmpty
    , s"either isWord [$isWord] or nodeByLetter.nonEmpty [${nodeByLetter.nonEmpty}] must be true")

  def merge(that: Node): Node = {
    //@tailrec
    def recursive(cursor: (Node, Node) = (this, that)): Node = {
      cursor match {
        case (Node.Terminal, Node.Terminal) =>
          Node.Terminal
        case (left, Node.Terminal) =>
          if (left.isWord)
            left
          else
            left.copy(isWord = true)
        case (Node.Terminal, right) =>
          if (right.isWord)
            right
          else
            right.copy(isWord = true)
        case (left, right) =>
          val lettersToMerge =
            left.nodeByLetter
              .keySet
              .filter(
                letter =>
                  right.nodeByLetter.keySet.contains(letter)
                    && (left.nodeByLetter(letter) != right.nodeByLetter(letter)))
          if (lettersToMerge.isEmpty)
            Node(
                left.isWord || right.isWord
              , right.nodeByLetter ++ left.nodeByLetter)
          else {
            val nodeKeysAll = (left.nodeByLetter.keySet ++ right.nodeByLetter.keySet)
              .toList
              .sorted
            val nodes = nodeKeysAll
              .map(
                letter =>
                  if (lettersToMerge.contains(letter)) {
                    //this call fails the @tailrec annotation
                    recursive(left.nodeByLetter(letter), right.nodeByLetter(letter))
                  } else
                    left.nodeByLetter.getOrElse(letter, right.nodeByLetter(letter))
              )
            val nodeByLetter = {
              nodes
                .zip(nodeKeysAll)
                .map(_.swap)
                .toMap
            }

            Node(
                left.isWord || right.isWord
              , nodeByLetter
            )
          }
      }
    }

    recursive()
  }
}

当方法

@tailrec
中的
merge
行未注释时,该行...

recursive(left.nodeByLetter(letter), right.nodeByLetter(letter))

...用红色波浪线突出显示(在 IntelliJ 中)并报告错误...

Recursive call not in tail position (in @tailrec annotated method)
.


这是我用来确保生成的函数正常工作的示例数据:

object Main {
  def main(args: Array[String]): Unit = {
    //cat
    val t = Node.Terminal
    val at = Node(false, Map('t' -> t))
    val cat = Node(false, Map('a' -> at))
    val catRoot = Node(false, Map('c' -> cat))
    //camp - intentionally not in alpha order
    val p = Node.Terminal
    val mp = Node(true, Map('p' -> p))
    val amp = Node(false, Map('m' -> mp))
    val camp = Node(false, Map('a' -> amp))
    val campRoot = Node(false, Map('c' -> camp))


    val root = catRoot.merge(campRoot)
    println("----------------")
    println("root: " + root)
  }
}

输出应如下所示:

----------------
root: Node(false,Map(c -> Node(false,Map(a -> Node(false,Map(t -> Node(true,Map()), m -> Node(true,Map(p -> Node(true,Map())))))))))

原始发布的代码不正确。

它没有达到预期的效果,更不用说不是尾递归了。我已按照 StackOverflow 中有关“更新问题”的规则保留了它。更正后的代码在上面。

case class Node(isWord: Boolean, nodeByLetter: Map[Char, Node]) {
  //@tailrec
  final def merge(that: Node): Node = {
    val mergedIsWord = this.isWord || that.isWord
    val mergedNodes =
      (this.nodeByLetter.keySet ++ that.nodeByLetter.keySet)
        .map(letter =>
          (
              letter
            , (this.nodeByLetter.get(letter), that.nodeByLetter.get(letter)) match {
                case (Some(thisNode), Some(thatNode)) =>
                  thisNode.merge(thatNode)
                case (Some(thisNode), None) =>
                  thisNode
                case (None, Some(thatNode)) =>
                  thatNode
                case _ =>
                  throw new IllegalStateException("should never get here")
              }))
        .toMap

    Node(mergedIsWord, mergedNodes)
  }
}
scala tree tail-recursion self-reference
1个回答
0
投票

总结:

OP 标题中括号问题的答案...

...(或者甚至可能)?

...是“是”。

OP 标题中完整问题的答案...

如何在 Scala 中的自引用树结构上创建尾递归合并方法?

...就是“只要有任何需要遵循递归调用的情况,就转向基于堆的策略,甚至在返回路径之前进行额外的递归调用。”

详情:

问题是可以解决的,但使用

@tailrec
注释则不容易解决。相反,它更简单地需要使用称为“trampoline”的 FP 递归概念。这个概念也称为 CPS(连续传球风格)。 由于这篇结构良好且呈现的关于什么以及如何使用蹦床的

文章

(特别排除了我在Reddit上探索理解的令人费解的挥手方法

sequence
),我能够得出一个完全有效的答案,您可以在下面看到详细信息。 import scala.annotation.tailrec import scala.util.control.TailCalls._ object Node { val Terminal: Node = Node(true, Map.empty) def encode(word: String): Node = word .reverse .foldLeft(Node.Terminal) { (node, letter) => Node(false, Map(letter -> node)) } } final case class Node( isWord: Boolean , nodeByLetter: Map[Char, Node] ) { require( isWord || nodeByLetter.nonEmpty , s"either isWord [$isWord] or nodeByLetter.nonEmpty [${nodeByLetter.nonEmpty}] must be true") def find(chars: String): Boolean = { @tailrec def recursive(charsRemaining: String = chars, node: Node = this): Boolean = charsRemaining match { case "" => node.isWord case charsRemainder => node.nodeByLetter.get(charsRemainder.head) match { case Some(nextNode) => recursive(charsRemainder.tail, nextNode) case None => false } } recursive() } def merge(that: Node): Node = { def sequence[A](listTailRecA: List[TailRec[A]]): TailRec[List[A]] = listTailRecA .reverse .foldLeft(done(Nil): TailRec[List[A]]) { (tailRecListA, tailRecA) => tailRecA map ((_: A) :: (_: List[A])).curried flatMap tailRecListA.map } def recursive(cursor: (Node, Node) = (this, that)): TailRec[Node] = { cursor match { case (Node.Terminal, Node.Terminal) => done(Node.Terminal) case (left, Node.Terminal) => if (left.isWord) done(left) else done(left.copy(isWord = true)) case (Node.Terminal, right) => if (right.isWord) done(right) else done(right.copy(isWord = true)) case (left, right) => val lettersToMerge = left.nodeByLetter .keySet .filter( letter => right.nodeByLetter.keySet.contains(letter) && (left.nodeByLetter(letter) != right.nodeByLetter(letter))) if (lettersToMerge.isEmpty) done(Node( left.isWord || right.isWord , right.nodeByLetter ++ left.nodeByLetter)) else { val nodeKeysAll = (left.nodeByLetter.keySet ++ right.nodeByLetter.keySet) .toList .sorted val listTailRecNode = nodeKeysAll .map( letter => if (lettersToMerge.contains(letter)) tailcall(recursive(left.nodeByLetter(letter), right.nodeByLetter(letter))) else done(left.nodeByLetter.getOrElse(letter, right.nodeByLetter(letter))) ) val tailRecListNode = sequence(listTailRecNode) tailRecListNode .map( ln => { val nodeByLetter = ln.zip(nodeKeysAll).map(_.swap).toMap Node( left.isWord || right.isWord , nodeByLetter ) }) } } } recursive().result } }

© www.soinside.com 2019 - 2024. All rights reserved.