【问题标题】:Assign Consecutive Numbers to Elements in a BST为 BST 中的元素分配连续编号
【发布时间】:2013-07-10 08:13:07
【问题描述】:

所以我试图严格使用递归(没有标准的前奏函数)将连续数字添加到 BST 中的元素。这是我目前所拥有的:

data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show)
leaf x = Node x Empty Empty 

number' :: Int -> Tree a -> Tree (Int, a)
number' a Empty = Empty    
number' a (Node x xl xr) = Node (a,x) (number' (a+1) xl) (number' (a+1)  xr) 

number :: Tree a -> Tree (Int, a)
number = number' 1

number' 是一个辅助函数,它带有“a”作为计数器。它应该在每个递归调用中加 1,所以我不确定它为什么在做它正在做的事情。 到目前为止,元素的级别已分配给每个元素。我想将第一个元素分配为 1,将 2 左侧的元素,3 左侧的元素等分配给每个元素。每个元素都应该分配一个 +1,并且不应重复任何数字。提前致谢。

【问题讨论】:

  • 您需要一个帮助器,它不仅返回编号的子树,还返回编号应继续使用的编号。
  • 所以我的计数器 a 应该是“编号继续的编号”,而我的函数编号应该是“返回编号子树的助手”?
  • 我不确定,但我怀疑你误解了这一点。 “...返回(不仅是 x,还有 y)。”你的助手必须给你两件事,一个数字和一棵树。
  • 好的,所以我将类型定义更改为 number' :: Int -> Tree a -> (Tree (Int, a), Int) 所以每次递归调用都会返回下一个数字,但我我仍然没有得到正确的答案。
  • 您的实现现在看起来如何?

标签: haskell recursion binary-tree


【解决方案1】:

我想先解释一下为什么问题中的代码会分配级别编号。这将直接引导我们找到两种不同的解决方案,一种是通过缓存,一种是基于同时进行两次遍历。最后,我展示了第二个解决方案与其他答案提供的解决方案之间的关系。

问题中的代码需要改变什么?

问题中的代码将级别编号分配给每个节点。我们可以通过查看number' 函数的递归情况来理解为什么代码会这样:

number' a (Node x xl xr) = Node (a,x) (number' (a+1) xl) (number' (a+1)  xr) 

请注意,我们对两个递归调用使用相同的数字a + 1。因此,两个子树中的根节点将被分配相同的编号。如果我们希望每个节点有不同的编号,我们最好将不同的编号传递给递归调用。

我们应该将什么数字传递给递归调用?

如果我们想根据从左到右的前序遍历来分配数字,那么a + 1对于左子树的递归调用是正确的,但对于右子树的递归调用则不正确。相反,我们希望留下足够的数字来注释整个左子树,然后开始用下一个数字来注释右子树。

我们需要为左子树保留多少个数字?这取决于子树的大小,由该函数计算得出:

size :: Tree a -> Int
size Empty = 0
size (Node _ xl xr) = 1 + size xl + size xr

回到number' 函数的递归情况。在左子树某处注释的最小数字是a + 1。在左子树某处注释的最大数字是a + size xl。所以右子树可用的最小数字是a + size xl + 1。这种推理导致number' 的递归案例的以下实现正常工作:

number' :: Int -> Tree a -> Tree (Int, a)
number' a Empty = Empty    
number' a (Node x xl xr) = Node (a,x) (number' (a+1) xl) (number' (a + size xl + 1)  xr) 

不幸的是,这个解决方案有一个问题:它太慢了。

为什么size 的解决方案很慢?

函数size 遍历整个树。函数number' 也遍历整个树,并在所有左子树上调用size。这些调用中的每一个都将遍历整个子树。所以总的来说,函数size 在同一个节点上执行了不止一次,当然它总是返回相同的值。

调用size时如何避免遍历树?

我知道两种解决方案:要么我们通过缓存所有树的大小来避免在 size 的实现中遍历树,要么我们首先通过对节点编号并计算大小来避免调用 size遍历。

我们如何在不遍历树的情况下计算大小?

我们在每个树节点中缓存大小:

data Tree a = Empty | Node Int a (Tree a) (Tree a) deriving (Show)

size :: Tree a -> Int
size Empty = 0
size (Node n _ _ _) = n

请注意,在sizeNode 情况下,我们只返回缓存的大小。所以这种情况不是递归的,size 不会遍历树,我们上面实现number' 的问题就消失了。

但是关于size 的信息必须来自某个地方!每次我们创建Node 时,我们都必须提供正确的大小来填充缓存。我们可以将此任务交给智能构造函数:

empty :: Tree a
empty = Empty

node :: a -> Tree a -> Tree a -> Tree a
node x xl xr = Node (size xl + size xr + 1) x xl xr

leaf :: a -> Tree a
leaf x = Node 1 x Empty Empty

只有node 是真正需要的,但为了完整起见,我添加了另外两个。如果我们总是使用这三个函数之一来创建树,那么缓存的大小信息将始终正确。

以下是适用于这些定义的number' 版本:

number' :: Int -> Tree a -> Tree (Int, a)
number' a Empty = Empty    
number' a (Node _ x xl xr) = node (a,x) (number' (a+1) xl) (number' (a + size xl + 1)  xr)

我们必须调整两件事:在Node 上进行模式匹配时,我们忽略了大小信息。而在创建Node时,我们使用智能构造函数node

这很好用,但它的缺点是必须更改树的定义。一方面,无论如何缓存大小可能是个好主意,但另一方面,它使用一些内存并且它迫使树是有限的。如果我们想在不改变树定义的情况下实现一个快速的number' 怎么办?这就引出了我承诺的第二个解决方案。

我们如何在不计算大小的情况下给树编号?

我们不能。但是我们可以在一次遍历中给树编号并计算大小,避免多次size 调用。

number' :: Int -> Tree a -> (Int, Tree (Int, a))

已经在类型签名中,我们看到这个版本的number' 计算了两条信息:结果元组的第一个组件是树的大小,第二个组件是带注释的树。

number' a Empty = (0, Empty)
number' a (Node x xl xr) = (sl + sr + 1, Node (a, x) yl yr) where
  (sl, yl) = number' (a + 1) xl
  (sr, yr) = number' (a + sl + 1) xr

实现从递归调用分解元组并组合结果的组件。请注意,sl 类似于上一个解决方案中的 size xlsr 类似于 size xr。我们还必须给带注释的子树命名:yl 是带有节点号的左子树,所以就像前面解决方案中的 number' ... xlyr 是带有节点号的右子树,所以就像 @987654366 @在上一个解决方案中。

我们还必须将number 更改为仅返回number' 结果的第二部分:

number :: Tree a -> Tree (Int, a)
number = snd . number' 1

我认为在某种程度上,这是最清晰的解决方案。

我们还能改进什么?

之前的解决方案通过返回子树的大小来工作。然后使用该信息来计算下一个可用节点号。相反,我们也可以直接返回下一个可用的节点号。

number' a Empty = (a, Empty)
number' a (Node x xl xr) = (ar, Node (a, x) yl yr) where
  (al, yl) = number' (a + 1) xl
  (ar, yr) = number' al xr

请注意,al 类似于前面解决方案中的 a + sl + 1ar 类似于 a + sl + sr + 1。显然,此更改避免了一些添加。

这本质上是 Sergey 回答的解决方案,我希望这是大多数 Haskelers 会写的版本。您还可以在状态 monad 中隐藏 aalar 的操作,但我认为这对于这样一个小例子没有帮助。 Ankur 的回答显示了它的样子。

【讨论】:

  • 非常感谢!我喜欢如何向我展示几个解决方案并深入解释它们是如何工作的。我从中学到了很多,再次感谢您!
  • 我有一个问题,你的最终解决方案中的 yl 和 yr 是什么?
  • ylyr 是左右子树的中间结果,已经带有节点号。我编辑了答案以试图解释这一点。
【解决方案2】:
data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show)

number :: Tree a -> Tree (Int, a)
number = fst . number' 1 

number' :: Int -> Tree a -> (Tree (Int, a), Int)
number' a Empty = (Empty, a)
number' a (Node x l r) = let (l', a') = number' (a + 1) l
                             (r', a'') = number' a' r
                          in (Node (a, x) l' r', a'')

*Tr> let t = (Node 10 (Node 20 (Node 30 Empty Empty) (Node 40 Empty Empty)) (Node 50 (Node 60 Empty Empty) Empty))
*Tr> t
 Node 10 (Node 20 (Node 30 Empty Empty) (Node 40 Empty Empty)) (Node 50 (Node 60 Empty Empty) Empty)
*Tr> number t
Node (1,10) (Node (2,20) (Node (3,30) Empty Empty) (Node (4,40) Empty Empty)) (Node (5,50) (Node (6,60) Empty Empty) Empty)

【讨论】:

  • 我喜欢你的方法,但我无法让它发挥作用,你认为你能解决它吗?
  • @user2548080 已修复并经过测试。
【解决方案3】:

正如 cmets 在您的问题中所建议的那样,每次调用 number 都应该返回一个整数,该整数还需要进一步用于下一组节点。这使得函数的签名为:

Tree a -> Int -> (Tree (Int,a), Int)

看看它的最后一部分,它看起来像是 State monad 的候选者,即state -> (Val,state)

下面的代码展示了如何使用 State monad 做到这一点。

import Control.Monad.State

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

myTree :: Tree String
myTree = Node "A" (Node "B" (Node "D" Empty Empty) (Node "E" Empty Empty)) (Node "C" (Node "F" Empty Empty) (Node "G" Empty Empty))

inc :: State Int ()
inc = do
  i <- get
  put $ i + 1
  return ()

number :: Tree a -> State Int (Tree (Int,a))
number Empty = return Empty
number (Node x l r) = do
  i <- get
  inc
  l' <- number l
  r' <- number r
  return $ Node (i,x) l' r'

main = do
    putStrLn $ show (fst (runState (number myTree) 1))

【讨论】:

    猜你喜欢
    • 2014-07-19
    • 1970-01-01
    • 2019-02-06
    • 2013-10-08
    • 1970-01-01
    • 1970-01-01
    • 2016-08-19
    • 2018-08-21
    • 1970-01-01
    相关资源
    最近更新 更多