带有未标记节点的树搜索

时间:2019-09-16 00:08:36

标签: ocaml

如果我有一个带有整数叶子的未标记树。这么说Node(Node(1,2),Node(2,3))。我希望有一个函数将这个未标记的树变成一个标记树,其中节点数据值是两个叶子的总和。因此对于此示例Node(Node(1、3、2),8,Node(2、5、3))。如果未标记的树可能不平衡,我该怎么做?我已经尝试过考虑使用每个节点/叶子的深度的解决方案,但是我不确定该怎么做。

2 个答案:

答案 0 :(得分:1)

树是否平衡对注释树没有影响。

@ivg指出,示例代码Node( Node(1,2) , Node(2,3) )没有意义,因为节点同时存储了具有不同类型的子节点和数字。正确的方法是:

type tree =
  | Leaf of int (* Leaves store numbers *)
  | Node of tree * tree

let tree = Node(Node(Leaf 1, Leaf 2), Node(Leaf 2, Leaf 3))

其中每个节点(包括内部节点)可以具有标签的树可以被编码为

type tree' =
  | Leaf' of int
  | Node' of tree' * int * tree'

let tree' = Node'(Node'(Leaf' 1, 3, Leaf' 2), 8, Node'(Leaf' 2, 5, Leaf' 3))

要注释树:

let weight : tree' -> int = function
  | Leaf' n -> n (* The weight of a leaf is its data *)
  | Node'(_, n, _) -> n (* The weight of a node is its middle value *)

let rec annot_tree : tree -> tree' = function
  | Leaf n -> Leaf' n (* Base case *)
  | Node(l, r) ->
      let labeled_l = annot_tree l in (* Annotate left child *)
      let labeled_r = annot_tree r in (* Annotate right child *)
      Node'(labeled_l, weight labeled_l + weight labeled_r, labeled_r)

答案 1 :(得分:-1)

术语

首先,让我们弄清楚什么是带标签的树和无标签的树以及如何将它们表示为数据结构。

  

如果我有一个带有整数叶子的未标记树。

不存在带有整数叶的未标记树之类的东西。 tree可以标记为标签,即为节点分配标签,也可以为未标记,即为节点未分配任何标签。

通常,当我们在计算机科学的背景下谈论树木时,我们会考虑标记树。当我们谈论标签树时,我们通常会考虑每个节点都有标签的树。

仅在叶节点上存储数据通常效率低下,因为我们必须使用具有更多层的树来存储相同的元素集,因为k元树的每一层最多可以存储k ^(h-1)个元素,其中h是图层的深度。而且,如果我们不使用内部节点来存储数据,我们最终将丢弃k ^(h-2)个节点。尽管如此,此类树仍然存在于生产中,并在某些B+ treesHuffman树等特殊应用中找到其用途。

现在,让我们看一下unlabeled trees。尽管它们是树木的本质,但它们在计算机科学中并不经常使用,并且具有数学意义而不是实用性。未标记的树仅编码结构,而仅编码结构,因为没有与节点相关联的东西。在combinatorics中研究了未标记的树,这些树可用于表示分子结构,并且通常可表示不同种类的自动机并研究结构之间的同构。

树数据结构

归纳定义

有两种编码树的方法。规范的方法是使用归纳数据类型,例如,在OCaml中,未标记的二叉树被编码为

type tree = Empty | Tree of tree * tree

乍一看,它看起来非常无用且虚假。但是我们可以用另一个简单的归纳定义进行类比,

type nat = Zero | Succ of nat 

是自然数的著名Peano编码。我们可以很容易地看到,未标记的树是Peano数字的扩展(例如,我们使用二进制Succ构造函数代替一元Tree构造函数)。同时我们可以发现Peano编码只不过是一个未标记的列表,例如,考虑列表数据结构的定义:

type 'a list = Empty | List of 'a * 'a list

因此,尽管事实是,它可以编码的唯一概念是其长度-这是自然数的本质-计数的想法。

未标记的树也是如此,因为它们表示决策的简单思想,每个未标记的树在决策空间中编码一个不同的元素。

尽管将数字存储为列表和决策树(如树)看起来像是在浪费空间,但它们仍在Curry-Howard Isomorphism的面使用,在这里well-known中每个可构造的数据结构都充当证明项。因此,这种结构成为自动定理证明整个领域的研究对象。

将树表示为图形

adjacency list也是一种特殊的图,因此树的另一种自然表示形式是图,例如,使用{{3}}来表示图,我们可以下面对树的定义。

type tree = (node * node) list

此定义提出了一个问题,node是什么类型?确实,我们现在需要一个单独的类型来承载图形。通常,我们只需要能够在两个节点之间进行区分,因此定义了相等操作的任何数据类型就足够了。例如,我们可以使用intstring。因此,具有三个节点的简单树将表示为两个分支[1,2; 1,3]的集合,其中每个分支都是两个节点的有序元组。

这种表示的主要问题是它不够紧,因为可以构造不是树的树,例如[1,2; 2,1]是我们tree类型的格式正确的值,但它不是树,因为它有一个循环。因此,为了保留图的树结构,我们应始终在每次插入时检查是否创建了循环或树的任何其他不变式是否被破坏(例如,插入了交叉边)。另一个问题是,必须为每个新鲜节点提供不同整数的来源。

因此,树的归纳定义具有很好的属性-总是正确的。归纳树的每个实例都是通过构造证明的树。此外,在归纳定义中,每个新节点实际上都是在堆中创建的新对象,因此它们是自动区分的。基本上,是语言运行库负责为我们创建新节点。

但是,在图形表示中有什么好处,以及为什么在这里显示它,是因为它融合了节点和节点标签之间的差异,因此现在很容易混淆它。确实,如果我们已经需要提供一些数据类型来承载节点(仅使一个节点与另一个节点区分开),那么为什么不使用相同的数据类型来存储一些标签,即我们可以将标签本身用作承载集,例如["Bil", "Adam"; "Bil", "Eve"]。这确实是一种在计算中经常使用的表示形式。归纳定义并没有真正提供这种自由。

您的代码有问题

首先,您的数据定义不一致,例如表达式

 Node( Node(1,2) , Node(2,3) )

的定义不明确,因为您要将Node1这样的整数和2构造的值都传递给Node构造函数构造函数,例如Node (1,2)。因此,它不是类型正确的代码,因为Node(1,2)1具有相同的类型,这显然是错误的。

树是递归数据类型,因此它的构造函数应接受与树本身相同类型的值。当然,为了构造递归数据类型,您应该有一个非递归构造函数,以便您可以输入递归,例如,这是一个未标记树的示例

type tree = Empty | Node of tree * tree

如您所见,在此定义中,int或任何其他有效负载都不存在。这就是未标记树的样子。与您的示例相反,在该示例中,使用一些整数值对节点进行了参数设置。

现在,我们准备定义一个标记树。它显然是不同的类型,因此它应该具有不同的数据构造函数,例如 1

type ltree = LEmpty | LNode of ltree * int * ltree

现在,让我们看一个未标记和标记树的示例,例如,最接近您的尝试的示例是

let unlabeled = Node (Node (Empty,Empty), Node (Empty, Empty))

现在,让我们想象一下相应的标记树,其中每个节点都由两个子树的深度之和(例如,

)参数化
let labeled = LNode (LNode (LEmpty, 2, LEmpty), LNode (LEmpty, 2, LEmpty))

如果我要编写一个将从前一种表示转换为后者的函数,那么我将从height函数开始,该函数将采用未标记的树并计算其深度。然后,我将编写translate函数,该函数将每个未标记的节点递归映射到一个标记的节点。两者都是微不足道的,因此,我将其作为练习留给您。


1)我们已经为数据构造函数添加了前缀(以大写字母开头,例如LEmptyLNode),并且为类型构造函数添加了前缀(以小写字母开头) (例如,ltree)和Ll字母来将其与未标记的树区分开。最好使用模块为您的构造者正确命名,但这是完全不同的故事。