如何在Haskell中编写N-ary树遍历函数

时间:2017-06-27 15:58:37

标签: algorithm haskell recursion tree functional-programming

我需要遍历N-ary树,并在我预先访问时访问每个节点添加号码。我有这样定义的n-ary树:

data NT a = N a [NT a] deriving Show

示例: 如果我有以下树:

let ntree = N "eric" [N "lea" [N "kristy" [],N "pedro" [] ,N "rafael" []],N "anna" [],N "bety" []]

我想将其转换为

let ntree = N (1,"eric") [N (2,"lea") [N (3,"kristy") [],N (4,"pedro") [] ,N (5,"rafael") []],N (6,"anna") [],N (7,"bety") []]

“Preordedness”并不重要。

我想看看如何编写在级别之间传递值的函数,比如如何将数字传递给后继列表以及如何将更新的数字传递给父级,并将该号码传递给其他分支

到目前为止,我已经能够编写这样的函数:

traverse :: NT String -> String
traverse (N val []) =" "++val++" "
traverse (N val list) =val++" " ++ (concat $ map  traverse list)

输出

"eric lea  kristy  pedro  rafael  anna  bety "

编辑:问题是:

如何编写函数

numberNodes :: NT a -> NT (a,Int)

根据树的前序遍历对节点进行编号?

让我理解的难点是通过辅助数据,你能详细说明吗?

在这个具体案例中,它是一个Int,表示我遍历这棵树的“时间”或顺序。

3 个答案:

答案 0 :(得分:22)

第一次尝试:努力工作

对于n-ary树的情况,有三个事情正在发生:编号元素,编号树和树的编号列表。这将有助于单独对待它们。类型第一:

aNumber   :: a                -- thing to number
          -> Int              -- number to start from
          -> ( (a, Int)       -- numbered thing
             , Int            -- next available number afterwards
             )

ntNumber  :: NT a             -- thing to number
          -> Int              -- number to start from
          -> ( NT (a, Int)    -- numbered thing
             , Int            -- next available number afterwards
             )

ntsNumber :: [NT a]           -- thing to number
          -> Int              -- number to start from
          -> ( [NT (a, Int)]  -- numbered thing
             , Int            -- next available number afterwards
             )

请注意,这三种类型共享相同的模式。当你看到你所遵循的模式时,显然巧合,你知道你有机会学到一些东西。但是现在让我们继续努力,稍后再学习。

为元素编号很简单:将起始编号复制到输出中,并将其后续版本作为下一个可用元素返回。

aNumber a i = ((a, i), i + 1)

对于另外两个,模式(那个词再次出现)是

  1. 将输入拆分为顶级组件
  2. 依次为每个组件编号,将数字穿过
  3. 使用模式匹配(在视觉上检查数据)和第二个使用where子句(抓住输出的两个部分)很容易做第一个。

    对于树,顶级分割为我们提供了两个组件:元素和列表。在where子句中,我们按照这些类型的指示调用适当的编号函数。在每种情况下,"""输出告诉我们应该用什么代替""输入。同时,我们将数字穿过,所以整体的起始编号是第一个组件的起始编号," next"第一个组件的编号从第二个组件开始," next"第二个数字是" next"整数。

    ntNumber (N a ants) i0  = (N ai aints, i2) where
      (ai,    i1) = aNumber   a    i0
      (aints, i2) = ntsNumber ants i1
    

    对于列表,我们有两种可能性。空列表没有组件,因此我们直接返回它而不使用任何其他数字。 A"缺点"有两个组件,我们完全像以前一样,使用类型指示的相应编号函数。

    ntsNumber []           i  = ([], i)
    ntsNumber (ant : ants) i0 = (aint : aints, i2) where
      (aint,  i1) = ntNumber  ant  i0
      (aints, i2) = ntsNumber ants i1
    

    让我们试一试。

    > let ntree = N "eric" [N "lea" [N "kristy" [],N "pedro" [] ,N "rafael" []],N "anna" [],N "bety" []]
    > ntNumber ntree 0
    (N ("eric",0) [N ("lea",1) [N ("kristy",2) [],N ("pedro",3) [],N ("rafael",4) []],N ("anna",5) [],N ("bety",6) []],7)
    

    所以我们在那里。但我们开心吗?好吧,我不是。我有令人讨厌的感觉,我写了几次相同的类型三次和几乎相同的程序两次。如果我想为不同组织的数据(例如,你的二进制树)做更多的元素编号,我必须再次写同样的东西。 Haskell代码中的重复模式总是 错失机会:培养自我批评感并询问是否有更简洁的方法非常重要。

    第二次尝试:编号和线程

    我们在上面看到的两个重复模式是   1.类型的相似性,   2.数字线程的相似性。

    如果您匹配类型以查看相同的内容,您会注意到它们全部

    input -> Int -> (output, Int)
    

    用于不同的输入和输出。让我们给最大的公共组件命名。

    type Numbering output = Int -> (output, Int)
    

    现在我们的三种类型

    aNumber   :: a      -> Numbering (a, Int)
    ntNumber  :: NT a   -> Numbering (NT (a, Int))
    ntsNumber :: [NT a] -> Numbering [NT (a, Int)]
    

    你经常在Haskell中看到这样的类型:

                 input  -> DoingStuffToGet output
    

    现在,为了处理线程,我们可以构建一些有用的工具来处理和组合Numbering操作。要查看我们需要哪些工具,请查看在我们对组件进行编号后如何组合输出。 """部分输出总是通过应用一些不能编号的函数(通常是数据构造函数)来构建一些" thing"编号输出。

    为了处理这些功能,我们可以构建一个看起来很像[]案例的小工具,其中不需要实际的编号。

    steady :: thing -> Numbering thing
    steady x i = (x, i)
    

    不要以类型使它看起来好像steady只有一个参数的方式推迟:记住Numbering thing缩写了一个函数类型,所以真的有另一个{{ 1}}在那里。我们得到

    ->

    就像steady [] :: Numbering [a] steady [] i = ([], i) 的第一行一样。

    但是其他构造函数ntsNumberN呢?问(:)

    ghci

    我们使用 functions 作为输出进行编号操作,并且我们希望通过更多编号操作生成这些函数的参数,从而产生一个大的整体编号操作,其中数字穿过。该过程的一个步骤是为编号生成的函数提供一个编号生成的输入。我将其定义为中缀运算符。

    > :t steady N
    steady N :: Numbering (a -> [NT a] -> NT a)
    > :t steady (:)
    steady (:) :: Numbering (a -> [a] -> [a])
    

    与显式应用程序运算符($$) :: Numbering (a -> b) -> Numbering a -> Numbering b infixl 2 $$

    的类型进行比较
    $

    > :t ($) ($) :: (a -> b) -> a -> b 运算符是"编号申请"。如果我们能够做到正确,我们的代码将成为

    $$

    ntNumber :: NT a -> Numbering (NT (a, Int)) ntNumber (N a ants) i = (steady N $$ aNumber a $$ ntsNumber ants) i ntsNumber :: [NT a] -> Numbering [NT (a, Int)] ntsNumber [] i = steady [] i ntsNumber (ant : ants) i = (steady (:) $$ ntNumber ant $$ ntsNumber ants) i 一样(目前)。此代码仅执行数据重构,将构造函数和组件的编号过程联系在一起。我们最好给出aNumber的定义,并确保它获得正确的线程。

    $$

    在这里,我们的旧线程模式完成一次($$) :: Numbering (a -> b) -> Numbering a -> Numbering b (fn $$ an) i0 = (f a, i2) where (f, i1) = fn i0 (a, i2) = an i1 fn中的每一个都是一个函数,期望一个起始编号,整个an是一个函数,它得到起始编号fn $$ sn。我们通过线程编号,首先收集函数,然后收集参数。然后,我们进行实际应用并将最后一个" next"号。

    现在,请注意,在每行代码中,i0输入都作为参数输入到编号过程中。我们可以通过谈论进程而不是数字来简化此代码。

    i

    阅读此代码的一种方法是过滤掉所有ntNumber :: NT a -> Numbering (NT (a, Int)) ntNumber (N a ants) = steady N $$ aNumber a $$ ntsNumber ants ntsNumber :: [NT a] -> Numbering [NT (a, Int)] ntsNumber [] = steady [] ntsNumber (ant : ants) = steady (:) $$ ntNumber ant $$ ntsNumber ants Numberingsteady用途。

    $$

    并且您看到它看起来像是一个前序遍历,在处理元素后重建原始数据结构。我们使用做正确的事情,前提是ntNumber :: NT a -> ......... (NT (a, Int)) ntNumber (N a ants) = ...... N .. (aNumber a) .. (ntsNumber ants) ntsNumber :: [NT a] -> ......... [NT (a, Int)] ntsNumber [] = ...... [] ntsNumber (ant : ants) = ...... (:) .. (ntNumber ant) .. (ntsNumber ants) steady正确组合了进程

    我们可以尝试对$$

    执行相同的操作
    aNumber

    aNumber :: a -> Numbering a aNumber a = steady (,) $$ steady a $$ ???? 是我们实际需要的数字。我们可以构建一个适合该漏洞的编号过程:发出下一个数字的编号过程

    ????

    这是编号的本质,""" output是现在要使用的数字(这是起始编号)," next"数字输出是之后的一个。我们可以写

    next :: Numbering Int
    next i = (i, i + 1)
    

    简化为

    aNumber a = steady (,) $$ steady a $$ next
    

    在我们的过滤视图中,该

    aNumber a = steady ((,) a) $$ next
    

    我们所做的就是提出编号过程的想法"我们已经构建了正确的工具,可以使用这些流程进行普通函数式编程。线程模式变为aNumber a = ...... ((,) a) .. next steady的定义。

    编号并不是唯一可行的方式。试试这个......

    $$

    ...而且你还会得到更多东西。我只想提请注意> :info Applicative class Functor f => Applicative (f :: * -> *) where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b pure的类型。它们与<*>steady非常相似,但它们不仅适用于$$Numbering每种类型进程的类型类,它以这种方式工作。我现在没有说&#34;现在就学习Applicative!&#34;,只是建议一个旅行方向。

    第三次尝试:类型定向编号

    到目前为止,我们的解决方案针对的是一个特定的数据结构ApplicativeNT a显示为辅助概念,因为它在[NT a]中使用。如果我们一次只关注一个类型的层,我们可以使整个事情更加即插即用。我们根据编号树来定义编号树的列表。一般来说,如果我们知道如何为 stuff 的每个项目编号,我们知道如何编号 stuff 的列表。

    如果我们知道如何为NT a a编号,我们应该能够为b编号列表以获取 a的列表。我们可以抽象出如何处理每个项目&#34;。

    b

    现在我们旧的树名单编号功能变为

    listNumber :: (a -> Numbering b) -> [a] -> Numbering [b]
    listNumber na []       = steady []
    listNumber na (a : as) = steady (:) $$ na a $$ listNumber na as
    

    这几乎不值得命名。我们可以写

    ntsNumber :: [NT a] -> Numbering [NT (a, Int)]
    ntsNumber = listNumber ntNumber
    

    我们可以为树木本身玩同样的游戏。如果你知道如何编号,你就知道如何为一棵树编号。

    ntNumber :: NT a -> Numbering (NT (a, Int))
    ntNumber (N a ants) = steady N $$ aNumber a $$ listNumber ntNumber ants
    

    现在我们可以做这样的事情

    ntNumber' :: (a -> Numbering b) -> NT a -> Numbering (NT b)
    ntNumber' na (N a ants) = steady N $$ na a $$ listNumber (ntNumber' na) ants
    

    在这里,节点数据现在本身就是一个列表,但我们已经能够单独编号。我们的设备更具适应性,因为每个组件都与该类型的一层对齐。

    现在,试试这个:

    myTree :: NT [String]
    myTree = N ["a", "b", "c"] [N ["d", "e"] [], N ["f"] []]
    
    > ntNumber' (listNumber aNumber) myTree 0
    (N [("a",0),("b",1),("c",2)] [N [("d",3),("e",4)] [],N [("f",5)] []],6)
    

    这很像我们刚刚做过的事情,其中​​> :t traverse traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b) fNumbering有时是列表,有时是树。

    t类捕获了类型成形器的含义,它允许您通过存储的元素来处理某种过程。同样,您使用的模式非常普遍并且已被预料到。学习使用Traversable可以节省大量的工作。

    最终...

    ...您将了解到traverse的工作已经存在于库中:它被称为Numbering,它属于{{1} } class,这意味着它也必须在State Int类中。掌握它,

    Monad

    以及使用其初始状态启动有状态进程的操作,就像我们对Applicative的投入一样,就是这样:

    import Control.Monad.State
    

    我们的0操作变为

    > :t evalState
    evalState :: State s a -> s -> a
    

    其中next是访问状态的进程,next' :: State Int Int next' = get <* modify (1+) 创建一个更改状态的进程,get表示&#34;但也执行&#34;。

    如果您使用语言扩展名pragma

    启动文件
    modify

    你可以像这样声明你的数据类型

    <*

    和Haskell会为你写{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}

    你的程序然后变成一行......

    data NT a = N a [NT a] deriving (Show, Functor, Foldable, Traversable)
    

    ...但是这一行的旅程涉及很多&#34;装瓶模式&#34;步骤,需要一些(希望是有益的)学习。

答案 1 :(得分:1)

我会在获得一些进展后立即更新此答案。

现在我将问题从n-ary树减少到二叉树。

data T a = Leaf a | N (T a) a (T a) deriving Show

numberNodes:: T a -> T (a,Int)
numberNodes tree = snd $ numberNodes2 tree 0

numberNodes2:: T a -> Int -> (Int,  T (a,Int))
numberNodes2 (Leaf a) time = (time,Leaf (a,time))
numberNodes2 (N left nodeVal right) time = (rightTime, N leftTree (nodeVal,time) rightTree  )
where (leftTime,leftTree) = numberNodes2 left (time+1)
      (rightTime,rightTree) = numberNodes2 right (leftTime+1)

函数numberNodes从这棵树创建:

let bt = N (N (Leaf "anna" ) "leo" (Leaf "laura")) "eric" (N (Leaf "john")  "joe" (Leaf "eddie"))

以下树:

N (N (Leaf ("anna",2)) ("leo",1) (Leaf ("laura",3))) ("eric",0) (N (Leaf ("john",5)) ("joe",4) (Leaf ("eddie",6)))

现在只需要为n-ary树重写它...(我不知道怎么做,任何提示?)

答案 2 :(得分:1)

@pigworker的

This answer非常好,我从中学到了很多东西。

但是,我相信我们可以使用Data.Traversable中的mapAccumL来实现非常相似的行为:

{-# LANGUAGE DeriveTraversable #-}

import           Data.Traversable
import           Data.Tuple

-- original data type from the question
data NT a = N a [NT a]
    deriving (Show, Functor, Foldable, Traversable)

-- additional type from @pigworker's answer
type Numbering output = Int -> (output, Int)

-- compare this to signature of ntNumber
-- swap added to match the signature
ntNumberSimple :: (NT a) -> Numbering (NT (a, Int))
ntNumberSimple t n = swap $ mapAccumL func n t
    where
        func i x = (i+1, (x, i))

我相信mapAccumL在引擎盖下使用的是相同的State monad,但至少它对调用者完全隐藏了。