【问题标题】:Difficulty implementing Huffman tree in Haskell在 Haskell 中难以实现 Huffman 树
【发布时间】:2019-10-24 02:40:17
【问题描述】:

我正在尝试学习 Haskell,但发现它真的很难,而且网上资源也不多。我似乎对递归调用的外观缺乏了解,并且希望能指出正确的方向。我正在尝试接收一棵树并返回每个叶节点以及存储在那里的符号,以及到达那里的路径。 (因此输入 (Fork (Leaf x) (Leaf y)) 将具有输出 [(x,[False]) ,(y,[True])] )。我的代码如下所示:

data htree a = Leaf a | Fork (htree a) (htree a) deriving (Show, Eq)

encode :: htree a -> [(a, [Bool])]
encode (Leaf a) = [(a, ????)]

我知道这没什么大不了的。我已经确定了基本情况,即每当你到达一片叶子时,你返回存储在叶子上的符号,以及你到达那里的路径。左为假,右为真。我不确定如何将所有这些信息放在一起以继续我的代码。我会很感激这里的任何指导。

【问题讨论】:

  • 您选择了一个相当重要的示例来了解递归的工作原理。尝试遍历列表。
  • 我认为实际上我非常了解如何遍历列表,并且已经编写了很多带有列表的函数。我掌握了这种思维的基础,但我无法将其应用于任何事情,这令人沮丧。
  • there aren't a lot of online resources 是否有您认为最有帮助的特定类型的资源?很多人推荐“Learn you a Haskell”。它是在线免费的。这是关于递归的章节:learnyouahaskell.com/recursion

标签: haskell functional-programming huffman-code


【解决方案1】:

考虑Fork。它有两个子树,每个子树都有一些编码。

假设左子树编码为:

[(x, pathToX), (y, pathToY)]

假设正确的子树编码是:

[(a, pathToA), (b, pathToB)]

现在,你能看到整个 fork 的编码应该是什么吗?应该是这样的:

[(a, True : pathToA), (b, True : pathToB), (x, False : pathToX), (y, False : pathToY)]

你同意吗?如果没有,请考虑一下。也许通过一些小例子来工作。直到您同意这种情况。

看看我在那里做了什么?我在左子树的每个路径前添加了False,然后在右子树的每个路径前添加了True

让我们用 Haskell 语法写下来:

encode (Fork left right) = prependToEach False (encode left) ++ prependToEach True (encode right)

现在你可能已经注意到我在这里作弊了:我使用了一个不存在的函数prependToEach。好吧,没关系,我们来定义吧!

prependToEach x list = map (prepend x) list

看到了吗?为列表的每个元素添加一个东西只是在列表上映射一个单元素的前置函数。

当然,我又作弊了:还没有prepend这样的功能。那就来一个吧!

prepend x (a, path) = (a, x : path)

你去吧!现在剩下的就是定义基本情况:Leaf 的路径应该是什么?好吧,根据您给出的示例,每个Leaf 都会有一条空路径,这反映了您不需要轮流从那片叶子到同一片叶子的事实:

encode (Leaf a) = [(a, [])]

现在,把它们放在一起:

encode :: HTree a -> [(a, [Bool])]
encode (Leaf a) = [(a, [])]
encode (Fork left right) = prependToEach False (encode left) ++ prependToEach True (encode right)
    where
    prependToEach x list = map (prepend x) list
    prepend x (a, path) = (a, x : path)

现在我们了解了它是如何构造的以及为什么构造它,我们可以通过使用列表推导来稍微缩短它(尽管我认为这一步非常可选):

encode :: HTree a -> [(a, [Bool])]
encode (Leaf a) = [(a, [])]
encode (Fork left right) = [(x, False : p) | (x, p) <- encode left] ++ [(x, True : p) | (x, p) <- encode right]

附:请注意,不能将类型命名为htree,因为 Haskell 中的类型名称必须大写。您可能会注意到我在最终的 sn-p 中将其重命名为 HTree

【讨论】:

  • 当,这是非常有用的信息。非常感谢!不过,如果您不介意回答,我确实有一个问题:为什么基本情况有外括号而递归情况没有?这似乎是一个静止的问题,但我仍然很难理解某些 Haskell 语法(就在前几天,你帮我解决了一些我正确思考但无法正确理解语法的代码,而我'想避免这些情况继续发生)。再次感谢您!
  • 基本情况和递归情况都返回一个列表。基本情况使用方括号语法构造列表。递归案例通过使用++ 运算符连接其他两个列表来构造列表。
【解决方案2】:

Fyodor's 的答案很好,但在使用(++) 时要小心。通常,如果您不喜欢它,它可能会导致您的代码对于特定输入运行非常缓慢,在这种情况下是不平衡的树。

原因是(N 个元素的列表)++(1 个元素的列表)必须构造一个包含 N+1 个元素的全新列表。因此,以这种方式一次只添加几个元素可能会很慢。

避免这种情况的一种方法是使用中间函数,而不是返回列表,而是返回传递列表时返回列表的函数。这样你就可以组合函数(速度很快)并在最后构造列表,现在将在不重新创建元素的情况下从左到右完成。

这是一个使用此方法的示例encode 函数:

data HTree a = Leaf a | Fork (HTree a) (HTree a) deriving (Show, Eq)

encode :: HTree a -> [(a, [Bool])]
encode tree = go [] tree [] where
  go :: [Bool] -> HTree a -> [(a, [Bool])] -> [(a, [Bool])]
  go path (Leaf leaf) = ((leaf, path):)
  go path (Fork left right) = go (False:path) left . go (True:path) right

请注意,您实际上并不需要类型签名,为了清楚起见,我只是将它们包括在内(这可能是一种很好的做法),但仅删除了 3 行:

encode tree = go [] tree [] where
  go path (Leaf leaf) = ((leaf, path):)
  go path (Fork left right) = go (False:path) left . go (True:path) right

请注意,这会返回从叶到根的路径,如果您希望它们从根到叶,您可以在最后反转它们或再次使用我的返回函数技巧。

【讨论】:

  • 连接列表确实需要在具有应用评估顺序的语言中重新分配整个列表,例如 Scala、F#、Ocaml 和几乎所有其他语言。然而,在 Haskell 中,由于其正常的评估顺序,连接不会执行重新分配,直到您强制结果,即使这样,重新分配也只会对整个结果发生一次,而不是每次连接一次。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-11
相关资源
最近更新 更多