10.3.1 是什么使得树处理很棘手?

10.3.1 是什么使得树处理很棘手?

 

    让我们看一个简单的处理树的例子。清单 10.15 声明了一个类型,表示一个整型的树,并显示一个递归函数,求树中所有值的和。

 

Listing 10.15 Tree data structure and summing elements (F# Interactive)

 

> type IntTree =
    | Leaf of int
    | Node of IntTree * IntTree;;
type IntTree = (...)

> let rec sumTree(tree) =
    match tree with
    | Leaf(n) -> n
    | Node(l, r) -> sumTree(l) + sumTree(r);;
val sumTree : IntTree -> int

 

    用来表示树的 IntTree 类型,是一个差别联合,有两个选项。注意,它其实与列表类型颇为相似的!树的值可以表示为包含一个整数的叶子,也可以一个节点。节点不包含数值,但它有两个 IntTree 类型的子树。求和的递归函数使用模式匹配,来区分这两种情况。对于叶子,返回数值,对于节点,它需要递归地对左、右子树的元素求和,并把这两个值加到一起。

    如果我们看一下 sumTree 函数,我们可以看到,这不是尾递归。它执行递归调用 sumTree,计算左子树中元素的和,然后需要执行一些额外的操作。更确切地说,它还要计算右子树中元素的和,最后把这两个数字相加。我们不知道如何以尾递归的方式写这个函数,因为,它要执行两个递归调用。最后这两个调用可以通过一些努力,做成尾递归,(通过使用一些各类的累加器参数),但我们还必须做一个普通的递归调用!这很烦人,因为,对于一些大型树,这个实现将导致堆栈溢出。

    我们需要一种不同的方法去思考。首先,让我们考虑树实际上可能是什么样子。图 10.6 显示了两个例子。

10-6

图 10.6 一个平衡树和不平衡树的例子。暗圈对应节点,亮圈包含值,对应叶子

 

    在图 10.6 中的平衡树,是一个相当典型的情况,??树的元素合理分布在左、右子树。这不是太坏,因为,我们永远不会结束特别深的递归。(对于当前的算法,最大递归深度就是树的根和叶之间存在的较长路径。)不平衡的例子要危险得多,在右侧有许多节点元素,所以,当我们递归处理时,必须做出大量的递归调用。处理这两种树之间的差异如清单 10.16 所示。

 

Listing 10.16 Summing tree using na?ve recursive function (F# Interactive)

 

> let tree = Node(Node(Node(Leaf(5), Leaf(8)), Leaf(2)),
                  Node(Leaf(2), Leaf(9)))
   sumTree(tree);;
val it : int = 26

> let numbers = List.init 100000 (fun _ -> rnd.Next(�C 50, 51);;
val numbers : int list = [29; -44; -1; 25; -33; 36; ...]

> let imbalancedTree =
     numbers |> List.fold (fun currentTree num �C>
       Node(Leaf(num), currentTree)) (Leaf(0));;
val imbalancedTree : IntTree

> sumTree(imbalancedTree);;
Process is terminated due to StackOverflowException.

 

    第一个命令创建了一个简单的树,并计算了叶子值的和。第二个命令使用 fold 函数创建的树,类似于在图 10.6 中的不平衡树,但更大。它首先有一个叶子,包含零,并在每一步在当前树的右侧,追加一个有左侧叶子的新节点,它从我们在清单 10.2 中创建的列表中取数,包含10 万个 �C50 到 50 之间的随机数。结果,我们就会得到一个 10 万个节点高度的树。当我们试图计算树的叶子的和时,会遇到堆栈溢出。这不是一个特别典型的情况,但我们仍然可以在处理树的代码中遇到它。幸运的是,连续给我们提供了一种方法,来写正常工作的函数,即使像这样的树。

你可能感兴趣的:(.net,代码,重构,职场,休闲)