【问题标题】:Haskell: Turning a tree into a mapHaskell:将树变成地图
【发布时间】:2013-07-16 17:32:55
【问题描述】:

基本上我想将 BST 树变成一个映射,其中节点是键,节点的出现次数是值。所以如果我输入这个:

toMap(第 13 页)

我会得到

> [(13,1)]

这是我目前所拥有的:

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

toMap' :: Int -> Tree a -> ([(a, Int)], Int)
toMap' a Empty = ([], a)
toMap' a (Node x xl xr) = ((x, a): xl' ++ xr', k)
                      where (xl', i) = toMap' (a+1) xl
                            (xr', k) = toMap' (i) xr

toMap :: Tree a -> [(a, Int)]
toMap = fst. toMap' 1

此程序返回一个地图,但值不正确。每个值都比前一个值大一(因此,如果有 3 个节点,则第三个节点的值将是 3)。我想我必须在每个新键上放置一个计数器,但我不确定如何。在此先感谢

【问题讨论】:

  • 另见this answer;如果你把它做成Foldable,你可以把它折叠成一张地图,这会很整洁。

标签: haskell map binary-tree


【解决方案1】:

假设您有一个函数 foldt 可以跨树折叠(以与您当前的应用程序无关的顺序),以及一些函数 insertIncr 插入或增加 Map a Int 中键的值,您可以应用一对一。

您将处理以下类型签名:

import Data.Map

foldt :: (a -> b -> b) -> b -> Tree a -> b
foldt f acc Empty = acc
foldt f acc (Node x l r) = acc'
    where accl = foldt f acc l
          accr = foldt f accl r
          acc' = f x accr

-- insert 1 if not present, increment if present
insertIncr :: Ord a => a -> Map a Int -> Map a Int
insertIncr = undefined

toMap' :: Ord a => Tree a -> Map a Int
toMap' = foldt insertIncr empty

insertIncr 函数可以使用例如Data.Map.insertWith。请注意,Ord 类型类对于将某些内容插入 Data.Map 是必需的。如果您更喜欢普通的[(a,Int)] 类型的映射,那么insertIncr 可以具有Eq a => a -> [(a,Int)] -> [(a,Int)] 类型。

编辑:将使用 adjustWithKey 的建议更改为 insertWith

【讨论】:

  • adjustWithKey 如果键不在地图中,则忽略插入。一个正确的定义是Data.Map.insertWith (+) k 1
【解决方案2】:

老实说,这是一个我只需通过组合分解来解决的案例。

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

toMap :: Ord a => Tree a -> [(a, Int)]
toMap = countDups . toList

请注意,我必须在 a 上添加一个额外的约束。它至少需要Eq 才能完全解决,但Ord 允许渐近更好的解决方案。

这里的基本思想是将解决方案分解为多个部分,然后再弄清楚每个部分。所以,下一部分是toList。我不会假设顺序很重要,因此我会选择前缀顺序,因为它很容易变得既懒惰又简单。

toList :: Tree a -> [a]
toList Empty = []
toList (Node a l r) = a : toList l ++ toList r

好的,简单明了。继续计算重复项。让我们也把它分解成几部分。

countDups :: Ord a => [a] -> [(a, Int)]
countDups = map (\xs -> (head xs, length xs)) . group . sort

好吧,我可能利用Data.List 中的groupsort 稍微作弊了。但另一方面,这正是group 旨在解决的问题。排序只是一个标准工具。

如果我导入了 Control.Arrow,我会将 lambda 替换为 (head &&& length)。但这只是一个标准的习语,并没有真正简化事情——它只是让它们的输入更简洁。

这种方法的主要思想是将问题分解成可以自己做一些有意义的事情的部分。然后将这些部分组合成一个完整的解决方案。有一种将Tree a 转换为[a] 的方法很方便。还不如有一个功能来做到这一点。一旦你这样做了,剩下的部分就是一个有用的逻辑,可用于处理列表。如果你把它分解,你会发现它是现有位列表功能的简单组合。

这通常是用任何编程语言解决问题的最佳方法之一 - 将大任务分解为小任务。在 Haskell 中这样做的好处在于,将较小的任务组合到整个过程中是一个非常简洁的过程。

【讨论】:

  • 非常感谢!这不仅有效,而且我学到了很多关于如何编写程序的知识。我还选择了一些将来有用的新功能。再次感谢!
  • @user2548080:Simons 解决方案的效率更高——出于好奇,我使用由数字 1..150 反复循环构建的树对其进行了测试。西蒙的解决方案可以在同一时间(大约半秒)内处理大约 200,000 个元素,而卡尔的解决方案可以处理 10,000 个元素。如果您有大量数据,请注意“简单”的事情,例如 sorting 它!此外,折叠是在高层次上表达许多类似问题的正确思维结构。
  • 另外,可以定义toList = foldt (:) []。 :) 对于渐近比较,进行树折叠并插入 Data.Map 是 O(n lg n) (n 表示元素的数量,lg n 表示插入基于树的映射),而 countDups 是O(n lg n + n + n)(n lg n 用于排序,n 用于分组,n 用于长度)。尽管排序是这里的渐近坏人,但我很想认为长度也是罪魁祸首。我不知道内存使用是否是这里真正的坏人。
  • @MikeHartl 性能几乎不重要。如果您简单之前专注于它,那么您就做错了。当我提出建议时,我知道这不是最快的。但在 99% 的用例中,这无关紧要。
猜你喜欢
  • 2019-11-09
  • 2011-01-15
  • 2015-08-29
  • 1970-01-01
  • 1970-01-01
  • 2017-12-15
  • 1970-01-01
  • 2011-05-24
  • 1970-01-01
相关资源
最近更新 更多