我需要遍历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,表示我遍历这棵树的“时间”或顺序。
答案 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)
对于另外两个,模式(那个词再次出现)是
使用模式匹配(在视觉上检查数据)和第二个使用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)
的第一行一样。
但是其他构造函数ntsNumber
和N
呢?问(:)
。
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
,Numbering
和steady
用途。
$$
并且您看到它看起来像是一个前序遍历,在处理元素后重建原始数据结构。我们使用值做正确的事情,前提是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;,只是建议一个旅行方向。
到目前为止,我们的解决方案针对的是一个特定的数据结构Applicative
,NT 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)
是f
而Numbering
有时是列表,有时是树。
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)
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,但至少它对调用者完全隐藏了。