在O中填充树的功能(深度)

时间:2015-02-25 02:50:31

标签: haskell tree

Purely Functional Data Structures有以下练习:

-- 2.5 Sharing can be useful within a single object, not just between objects.
-- For example, if the two subtress of a given node are identical, then they can 
-- be represented by the same tree.
-- Part a: make a `complete a Int` function that creates a tree of 
-- depth Int, putting a in every leaf of the tree.
complete :: a -> Integer -> Maybe (Tree a)
complete x depth 
 | depth < 0  = Nothing
 | otherwise  = Just $ complete' depth
                        where complete' d 
                                | d == 0    = Empty
                                | otherwise = let copiedTree = complete' (d-1) 
                                              in Node x copiedTree copiedTree

此实施是否在O(d)时间内运行?你能说出原因或原因吗?

2 个答案:

答案 0 :(得分:4)

代码的有趣部分是complete'函数:

complete' d 
  | d == 0    = Empty
  | otherwise = let copiedTree = complete' (d-1) 
                in Node x copiedTree copiedTree

Cirdec's answer所示,我们应该小心分析实施的每个部分,以确保我们的假设是有效的。作为一般规则,我们可以假设以下每个*

需要1个单位时间
  1. 使用数据构造函数构造值(例如,使用Empty创建空树或使用Node将值和两棵树转换为树)。

  2. 对值进行模式匹配,以查看其构建的数据构造函数以及应用数据构造函数的值。

  3. Guards和if / then / else表达式(使用模式匹配在内部实现)。

  4. Integer与0进行比较。

  5. Cirdec提到从Integer减去1的运算是整数大小的对数。正如他们所说,这实际上是Integer实现方式的工件。可以实现整数,这样只需要一步就可以将它们与0进行比较,并且只需要一步就可以将它们减1。为了保持一般性,可以安全地假设有一些函数c使得成本递减Integer的是c(深度)。


    现在我们已经完成了预备工作,让我们开始工作吧!通常情况下,我们需要建立一个方程组并解决它。设f(d)是计算complete' d所需的步数。然后第一个方程非常简单:

    f(0) = 2
    

    这是因为将d与0进行比较需要一步,另一步是检查结果是否为True

    另一个等式是有趣的部分。想想d > 0

    时会发生什么
    1. 我们计算d == 0
    2. 我们检查是True(不是)。
    3. 我们计算d-1(让我们调用结果dm1
    4. 我们计算complete' dm1,将结果保存为copiedTree
    5. 我们将Node构造函数应用于xcopiedTreecopiedTree
    6. 第一部分需要1步。第二部分迈出了一步。第三部分采用c(深度)步骤,第五步采用1步。那第四部分呢?好吧,这需要f(d-1)步,所以这将是一个递归定义。

      f(0) = 2
      f(d) = (3+c(depth)) + f(d-1)    when d > 0
      
      好的,现在我们正在用煤气烹饪!让我们计算f的前几个值:

      f(0) = 2
      f(1) = (3+c(depth)) + f(0) = (3+c(depth)) + 2
      f(2) = (3+c(depth)) + f(1)
           = (3+c(depth)) + ((3+c(depth)) + 2)
           = 2*(3+c(depth)) + 2
      f(3) = (3+c(depth)) + f(2)
           = (3+c(depth)) + (2*(3+c(depth)) + 2)
           = 3*(3+c(depth)) + 2
      

      您现在应该开始看到一种模式:

      f(d) = d*(3+c(depth)) + 2
      

      我们通常使用数学归纳来证明递归函数。

      基本情况:

      声明对于d = 0,因为0 *(3 + c(深度))+ 2 = 0 + 2 = 2 = f(0)。

      假设声明适用于d = D.然后

      f(D+1) = (3+c(depth)) + f(D)
             = (3+c(depth)) + (D*(3+c(depth))+2)
             = (D+1)*(3+c(depth))+2
      

      因此声明同样适用于D + 1。因此,通过归纳,它适用于所有自然数d。提醒一下,这得出complete' d需要

      的结论
      f(d) = d*(3+c(depth))+2
      

      时间。现在我们如何用大O表达呢?好吧,大O并不关心任何术语的常数系数,只关心最高阶项。我们可以安全地假设c(深度)&gt; = 1,所以我们得到

      f(d) ∈ O(d*c(depth))
      

      缩小到complete,这看起来像O(深度* c(深度))

      如果使用Integer递减的实际成本,则会得到O(深度* log(深度))。如果假装Integer递减为O(1),则会给出O(深度)。

      旁注:当你继续在Okasaki工作时,你最终会到达第10.2.1节,在那里你将看到一种方法来实现支持O(1)减量和O(1)添加的自然数(但不是有效的减法)。

      * Haskell的懒惰评估使得这一点不能完全正确,但是如果你假装所有内容都是严格评估的,那么你将获得真值的上限,这在这种情况下就足够了。如果你想学习如何分析使用懒惰来获得良好渐近边界的数据结构,你应该继续阅读Okasaki。

答案 1 :(得分:3)

理论答案

不,它不会在O(d)时间内运行。其asymptotic performanceInteger减法d-1支配,花费O(log d)时间。重复O(d)次,给出O(d log d)时间的渐近上限。

如果使用具有渐近最优Integer递减的O(1)表示,则此上限可以得到改善。 In practice we don't,因为即使对于难以想象的大值,渐近最优Integer实现也会变慢。

实际上Integer算术只占程序运行时间的一小部分。对于实际的“大”深度(小于机器字),程序的运行时间将由分配和填充内存来控制。对于较大的深度,您将耗尽计算机的资源。

实用答案

询问运行时系统的profiler

为了分析您的代码,我们首先需要确保它运行。 Haskell是懒惰的评估,因此,除非我们做一些事情以使树被完全评估,否则它可能不会。不幸的是,完全探索树将采取O(2^d)步骤。如果我们跟踪他们的StableName,我们可以避免强制我们已经访问过的节点。幸运的是,data-reify包已经提供了遍历结构并通过其内存位置跟踪被访问节点的信息。由于我们将使用它进行性能分析,我们需要安装它并启用性能分析(-p)。

cabal install -p data-reify

使用Data.Reify需要TypeFamilies扩展名和Control.Applicative

{-# LANGUAGE TypeFamilies #-}

import Data.Reify
import Control.Applicative

我们会重现您的Tree代码。

data Tree a = Empty | Node a (Tree a) (Tree a)

complete :: a -> Integer -> Maybe (Tree a)
complete x depth 
 | depth < 0  = Nothing
 | otherwise  = Just $ complete' depth
                        where complete' d 
                                | d == 0    = Empty
                                | otherwise = let copiedTree = complete' (d-1) 
                                              in Node x copiedTree copiedTree

使用data-reify将数据转换为图表需要我们为数据类型提供基本仿函数。基础仿函数是删除了显式递归的类型的表示。 Tree的基础仿函数为TreeF。为表示类型的递归出现添加了一个附加的类型参数,每个递归出现都被新参数替换。

data TreeF a x = EmptyF | NodeF a x x
    deriving (Show)

reifyGraph所需的MuRef实例要求我们提供mapDeRef来使用Applicative遍历结构并将其转换为基础仿函数。提供给mapDeRef的第一个参数,我将其命名为deRef,是我们如何转换结构的递归出现。

instance MuRef (Tree a) where
    type DeRef (Tree a) = TreeF a
    mapDeRef deRef Empty        = pure EmptyF
    mapDeRef deRef (Node a l r) = NodeF a <$> deRef l <*> deRef r

我们可以制作一个小程序来测试complete函数。当图表很小时,我们会打印出来看看发生了什么。当图形变大时,我们只会打印出它有多少个节点。

main = do
    d <- getLine
    let (Just tree) = complete 0 (read d)
    graph@(Graph nodes _) <- reifyGraph tree
    if length nodes < 30 
    then print graph
    else print (length nodes)

我将此代码放在名为profileSymmetricTree.hs的文件中。要编译它,我们需要使用-prof启用分析,并使用-rtsopts启用运行时系统。

ghc -fforce-recomp -O2 -prof -fprof-auto -rtsopts profileSymmetricTree.hs

运营时,我们会使用+RTS选项-p启用时间配置文件。我们将为第一次运行提供深度输入3

profileSymmetricTree +RTS -p
3
let [(1,NodeF 0 2 2),(2,NodeF 0 3 3),(3,NodeF 0 4 4),(4,EmptyF)] in 1

我们已经可以从图中看到节点在树的左侧和右侧之间共享。

分析器会生成一个文件profileSymmetricTree.prof

                                                                                individual     inherited
COST CENTRE                        MODULE                     no.     entries  %time %alloc   %time %alloc

MAIN                               MAIN                        43           0    0.0    0.7   100.0  100.0
 main                              Main                        87           0  100.0   21.6   100.0   32.5
  ...
  main.(...)                       Main                        88           1    0.0    4.8     0.0    5.1
   complete                        Main                        90           1    0.0    0.0     0.0    0.3
    complete.complete'             Main                        92           4    0.0    0.2     0.0    0.3
     complete.complete'.copiedTree Main                        94           3    0.0    0.1     0.0    0.1

它在entries列中显示complete.complete'已执行4次,而complete.complete'.copiedTree已经过3次评估。

如果你用不同的深度重复这个实验并绘制结果,你应该知道complete的实际渐近性能是什么。

以下是更深入的分析结果300000

                                                                                individual     inherited
COST CENTRE                        MODULE                     no.     entries  %time %alloc   %time %alloc

MAIN                               MAIN                        43           0    0.0    0.0   100.0  100.0
 main                              Main                        87           0    2.0    0.0    99.9  100.0
  ...
  main.(...)                       Main                        88           1    0.0    0.0     2.1    5.6
   complete                        Main                        90           1    0.0    0.0     2.1    5.6
    complete.complete'             Main                        92      300001    1.3    4.4     2.1    5.6
     complete.complete'.copiedTree Main                        94      300000    0.8    1.3     0.8    1.3