是否可以编写一个不可变的双向链表?

时间:2018-05-22 12:26:38

标签: scala functional-programming immutability immutable-collections

我觉得这个问题有点愚蠢,但我目前正在学习函数式编程,并完成了创建单链表的练习,它让我思考,是否有可能创建一个不可变的双向链表?

假设列表A :: B,在构造时,A需要知道B,但B也需要知道A.我在Scala中一直这样做,所以我不是确定它是否特定于Scala,但我无法想象它是如何工作的。

我不是在寻找替代品,因为我不需要这样做,我只是好奇。

1 个答案:

答案 0 :(得分:2)

是的,这是可能的。但通常不会这样做,因为与单链表不同,双链表没有任何可以重用的子结构,例如,当一个元素被删除时。而且,这样的列表似乎没有做任何不可变的Vector不能做的事情。

然而,让我们把它写下来,因为它很有趣。

简化问题:圆形双元素“列表”

作为热身,请看一下简化的问题:一个循环的双元素“列表”,其中两个节点相互引用:

case class HalfRing(val value: Int)(otherHalf: => HalfRing) {
  def next = otherHalf
}

object HalfRing {
  def fullRing(a: Int, b: Int): HalfRing = {
    lazy val ha: HalfRing = HalfRing(a){hb}
    lazy val hb: HalfRing = HalfRing(b){ha}
    ha
  }
}

这确实有效,我们可以构建这个小的双节点数据结构,并在其上循环运行几百万次迭代:

var r = HalfRing.fullRing(42, 58)
for (i <- 0 until 1000000) {
  r = r.next
  if (i % 100001 == 0) println(r.value)
}

输出:

58
42
58
42
58
42
58
42
58
42

循环演示的是:这是一个实际的数据结构,而不是一些奇怪的嵌套函数系列,它们在访问元素几次之后就会烧掉堆栈。

不可变的双链表

我决定用双链接连接的节点表示列表,两端有两个明确的Nil - 元素:

sealed trait DLL[+A] extends (Int => A)
case class LeftNil[+A]()(n: => DLL[A]) extends DLL[A] {
  def next = n
  def apply(i: Int) = next(i)
}
case class RightNil[+A]()(p: => DLL[A]) extends DLL[A] {
  def prev = p
  def apply(i: Int) = 
    throw new IndexOutOfBoundsException("DLL accessed at " + i)
}
case class Cons[+A](value: A)(p: => DLL[A], n: => DLL[A]) extends DLL[A] {
  def next = n
  def prev = p
  def apply(i: Int) = if (i == 0) value else next(i - 1)
}

apply - 部分基本上是不相关的,我只是添加了它,以便我可以稍后检查和打印内容。有趣的问题是:我们如何实际实例化这样的列表?这是一种将单个链表转换为双链表的方法:

object DLL {
  def apply[A](sll: List[A]): DLL[A] = {
    def build(rest: List[A]): (=> DLL[A]) => DLL[A] = rest match {
      case Nil => RightNil[A]() _
      case h :: t => {
        l => {
          lazy val r: DLL[A] = build(t){c}
          lazy val c: DLL[A] = Cons(h)(l, r)
          c
        }
      }
    }
    lazy val r: DLL[A] = build(sll){l}
    lazy val l: DLL[A] = LeftNil(){r}
    l
  }
}

这里发生的事情与上面的双元素环基本相同,但重复多次。我们只是以加入两个半环的方式保持连接,除了在这里我们首先将小Cons - 元素连接到列表的长尾,然后最后加入LeftNil第一个Cons

再一次,一个小的演示,一个“迭代器”,它继续在列表上来回运行几百万次迭代,偶尔会打印当前元素:

val dll = DLL((42 to 100).toList)

println((1 to 20).map(dll))

@annotation.tailrec 
def bounceBackAndForth(
  dll: DLL[Int], 
  maxIters: Int, 
  direction: Int = +1
): Unit = {
  if (maxIters <= 0) println("done")
  else dll match {
    case ln: LeftNil[Int] => bounceBackAndForth(ln.next, maxIters - 1, +1)
    case rn: RightNil[Int] => bounceBackAndForth(rn.prev, maxIters - 1, -1)
    case c: Cons[Int] => {
      if (maxIters % 100003 == 0) println(c.value)
      if (direction < 0) {
        bounceBackAndForth(c.prev, maxIters - 1, -1)
      } else {
        bounceBackAndForth(c.next, maxIters - 1, +1)
      }
    }
  }
}

bounceBackAndForth(dll, 1000000)

// cs_XIIIp4

备注:我没有发现递归build - 方法特别直观,我不能直接写下来而不在一张纸上乱涂几分钟。说实话,每次工作时我都会感到有些惊讶。

相关问题