2013-10-08 143 views
5

我正在扫描此代码,并且我想了解它是如何工作的。了解纯功能持久二叉树

有两种可能的树:空树的树和由一个整数和两个子树组成的树。不变量:对于每个节点,右侧的节点包含的整数值高于父节点,而左侧的节点包含低于父节点的整数值。

下面是代码:

abstract class IntSet{ 
    def incl(x: Int): IntSet   // include element x in the IntSet 
    def contains(x: Int): Boolean // is x an element of the set? 
    def union(other: IntSet): IntSet // union of this and other set 
} 

object Empty extends IntSet{ 
    def contains(x: Int): Boolean = false 
    def incl(x:Int): IntSet = new NonEmpty(x, Empty, Empty) 
    override def toString = "." 
    def union(other:IntSet): IntSet = other 
} 

class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet{ 

    def contains(x: Int): Boolean = 
    if  (x < elem) left contains x 
    else if (x > elem) right contains x 
    else true 

    def incl(x: Int): IntSet = 
    if  (x < elem) new NonEmpty(elem, left incl x, right) 
    else if (x > elem) new NonEmpty(elem, left, right incl x) 
    else this 

    override def toString = "{" + left + elem + right + "}" 

    def union(other:IntSet): IntSet = 
    ((left union right) union other) incl elem 

} 

如何使用这个数据结构?它如何实现持久性?它是如何工作的?

+0

请注意,这不是一个纯功能树。它是一棵不变的树,每个树操作都会在内存中创建一个可以保存的类实例。 – 0kcats

回答

7

代码直接映射到您提供的描述。

让我们举一个简单的例子来演示用法:首先有一个空集,比如说e 然后给它添加一个元素来获得另一个集合,比如s1。然后,您将有 2套,es1

val e = Empty 
e contains 42  // will be false 
// create s1 from e 
val s1 = e incl 1 // e does not change; it remains a reference to the Empty set. 
// Now we have s1, a set that should contain (1) and nothing else. 
s1 contains 1  // will be true 
s1 contains 42  // will be false 

我猜你所熟悉的斯卡拉速记,可以让你键入的 s1 incl 1代替s1.incl(1)

注意,这里不仅可以永远是一个空集,所以这是一样好:

val s1 = Empty incl 1 

那么,让我们说你想添加,说2得到另一套s2其元素 必须包括{1, 2}

val s2 = s1 incl 2 
s2 contains 1  // true 
s2 contains 2  // true 
s2 contains 3  // false 

所以任意一组的方法incl接受一个元件和返回一个新的组 - 它不 变化的集(其include方法被称为原始对象ob)。

我们有两种类型的树集;空和非空,并且每个具有用于incl一个实现:

// Empty 
def incl(x:Int): IntSet = new NonEmpty(x, Empty, Empty) 

读取:“添加元素为空(树)组产生另一组是一个非空的树只有一个根值为1的节点和空的左右子树。“

非空集具有构造函数参数elem,它表示树的根并且对NonEmpty中的所有方法都可见。

// Non-Empty 
def incl(x: Int): IntSet = 
    if  (x < elem) new NonEmpty(elem, left incl x, right) 
    else if (x > elem) new NonEmpty(elem, left, right incl x) 
    else this 

读取:(在上面的if-else的相反的顺序):

  • 添加元素x到非空集,其元素也是x 给你同一组(this
  • 添加元素x到非空集,其元件比01少 x 给你另一个集,其中:
    • 根元素是一样的原来设定
    • 子树是不变的 - 一样的,在原设定
    • 新权子树变成原来的右子树树 x添加到它“:right incl x
  • 添加元素x到非空集,其元件比x 给你另一个集,其中更大
    • 根元素是相同的原始集合
    • right子树不变 - 与原始集相同
    • 新左子树成为原始左子树 x添加到它“:left incl x

的‘持久性’是一个事实,即没有树或子树是是否会改变实现。在这个例子中

val s1 = Empty incl 1  // s1 is a tree with only a root(1) an no branches. 
val s2 = s1 incl 2   // s2 is another tree with - 
          // - the same root(1), 
          // - the same left-subtree as s1, (happens to be Empty) 
          // - a new subtree which in turn is a tree with - 
          // - the root element (2) 
          // - no left or right brances. 
s1 contains 1 // true 
s1 contains 2 // false 
s2 contains 1 // true 
s2 contains 2 // true 

val s3 = s2 incl -3  // s2.incl(-3) 
          // from s2 we get s3, which does not change s2's structure 
          // in any way. 
          // s3 is the new set returned by incl, whose 
          // - root element remains (1) 
          // - left subtree is a new tree that contains 
          // just (-3) and has empty left, right subtrees 
          // - right subtree is the same as s2's right subtree! 

s3.contains(-3) // true; -3 is contained by s3's left subtree 
s3.contains(1) // true; 1 is s3's root. 
s3.contains(2) // true; 2 is contained by s3's right subtree 
s3.contains(5) // false 

我们只使用incl从其它套派生集(树),在不改变原设定。这是因为在非常阶段,我们要么 -

  1. 回报新基于离开的人,而不是修改 现有结构,
  2. 回报现有的结构,因为它们的数据结构。

contains的工作方式相同:Empty有任何输入返回false的实现。 NonEmpty如果给定元素与它的根相同,或者它的左边或右边的子树都包含它,它会很快返回true!

+0

我们可以在永久树上实现删除操作吗? – pannu

4

让我们从incl开始。这是一个树上的方法,它接受一个元素并创建一个等于当前树但添加了元素的新树。它在不修改原始树的情况下执行此操作。这是处理这些树作为不可变数据结构的一部分,也是“持久”数据结构思想的核心。本质上,对于我们希望对树进行的任何更改,我们希望创建一棵新树并保留树的以前状态。我们也希望有效地创建新树,创建尽可能少的新节点,并链接到现有节点,而不会影响原始节点。

以一个例子下面的树:

4 
/\ 
    2 6 
/\ \ 
1 3 7 

如果我们想元素5添加到这一点,我们希望与落得:

 4 
/ \ 
    2  6 
/\ /\ 
1 3 5 7 

我们可以通过创建做到这一点包含指向包含2的现有节点(和附属子树)的元素4的新根节点以及包含6的新节点(其注意到调用的递归性质为new NonEmpty(elem, left, right **incl** x))指向包含6的新节点,该新节点包含5和现有节点包含7.因此只有三个节点被创建,并且四个现有节点被重用。请注意,这不会影响可继续引用包含1,2,3和7的叶节点的原始树。