为什么这个不可变的双向链表实现溢出堆栈

时间:2014-06-02 04:20:02

标签: scala recursion lazy-evaluation

作为练习,我想我会尝试在Scala中实现一个不可变的双向链表。目前lazy val导致堆栈溢出。有人可以解释为什么会这样吗?我非常确定递归函数通常会终止,但长度为3是一个非常小的数字,可以从终止函数创建溢出。似乎懒惰的事情意味着它会陷入某个循环中。

class Node(val prev: Option[Node], val next: Option[Node], val id: Int){
  override def toString = "["+id+"] "+next.toString
}

def addNodes(nNodes: Int, last: Node): Node = {
  if(nNodes > 0){
    lazy val newNode: Node = 
      new Node(Some(last), Some(addNodes(nNodes-1, newNode)),nNodes)
    newNode
  } else {
    new Node(Some(last), None, nNodes)
  }
}

def doublyLinked(n:Int) = {
  lazy val list: Node = new Node(None, Some(addNodes(n-2, list)),n-1)
  list
}

val x = doublyLinked(3)
println(x)

1 个答案:

答案 0 :(得分:6)

此处的问题不在于addNodes,而在于lazy val本身。

lazy val newNode: Node = 
      new Node(Some(last), Some(addNodes(nNodes-1, newNode)),nNodes)

要计算newNode,您需要调用addNodes(nNodes-1, newNode),但这意味着您需要newNode值。 lazy val内部是通过方法实现的,所以这是导致堆栈溢出的递归。

在没有懒惰的情况下构建循环不可变数据结构是不可能的,事实证明你的实现不够懒惰。尝试使用此结构(与您已有的addNodesdoublyLinked实现完全相同):

class Node(_prev: =>Option[Node], _next: =>Option[Node], val id: Int){
  lazy val prev = _prev
  lazy val next = _next
  override def toString = s"[$id]${next.toString}"
}

我们的想法是使prevnext按名称调用参数,并通过公共惰性值公开它们。这给了足够的懒惰来实现不可变的双向链表。在这种情况下,上面没有描述递归,因为按名称调用参数意味着在读取相应节点的Some(addNodes(nNodes-1, newNode))字段之前不会计算next

但请注意,不可变的双向链表是一种不方便的结构。为了向列表添加新元素,您必须从头开始重建它,与单链表相反:在单链表的前面添加元素是一个恒定时间操作,不需要重建整个清单。插入中间需要仅重建插入元素之前的部分。这就是它在功能语言中如此受欢迎的原因。双链表需要完全重建任何修改。