在Haskell中实现Huffman树的困难

在Haskell中实现Huffman树的困难,haskell,functional-programming,huffman-code,Haskell,Functional Programming,Huffman Code,我正在努力学习Haskell,但发现它真的很难,而且网上资源也不多。我似乎对递归调用的外观缺乏理解,如果能指出正确的方向,我将不胜感激。我试图获取一棵树并返回每个叶节点,其中包含存储在树中的符号,以及到达树的路径。(因此输入(Fork(Leaf x)(Leaf y))将具有输出[(x[False]),(y[True]))。我的代码如下所示: data htree a = Leaf a | Fork (htree a) (htree a) deriving (Show, Eq) encode :

我正在努力学习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, ????)]

我知道这没什么好谈的。我已经确定了基本情况,即无论何时到达某个叶,都返回存储在该叶上的符号,以及到达该叶的路径。左是假,右是真。我不知道如何把所有这些信息放在一起,以便继续我的代码。我希望能在这里得到任何指导。

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

假设左子树编码为:

[(x, pathToX), (y, pathToY)]
[(a, pathToA), (b, pathToB)]
假设正确的子树编码是:

[(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
的路径应该是什么?好的,根据您给出的示例,每个
都有一个空路径,这反映了一个事实,即您不需要轮流从该叶进入同一叶:

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])]
编码(叶a)=[(a,[])]
encode(Fork left-right)=[(x,False:p)|(x,p)答案是好的,但使用
(++)
时值得警惕。通常,如果您不满意,它会导致您的代码在特定输入下运行非常缓慢,在这种情况下是一个不平衡的树

原因是(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

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

您选择了一个非常重要的示例来了解递归的工作原理。试着从遍历列表开始。我认为,实际上我非常了解如何遍历列表,并且有自己的经验我在很多函数中加入了列表。我掌握了这种思维的基本知识,但我无法将其应用到任何事情上,这令人沮丧。
没有太多的在线资源
有什么特别的资源对你最有帮助吗?很多人建议“向你学习Haskell”。这是免费的。这是关于递归的一章:当,这是非常有用的。非常感谢你!如果你不介意回答的话,我确实有一个问题:为什么基本情况有外括号,而递归情况没有?这似乎是一个静止的问题,但我仍然很难理解某些Haskell语法(就在前几天,你帮我编写了一些我想对了但语法不正确的代码,我希望以后避免这些情况)。再次感谢!基本和递归案例都返回一个列表。基本案例使用括号语法构造列表。递归案例通过使用
++
运算符连接其他两个列表来构造列表。连接列表确实需要在具有应用程序求值ord的语言中重新分配整个列表呃,比如Scala、F#、Ocaml和几乎所有其他语言。但是,在Haskell中,由于其正常的求值顺序,连接在强制结果之前不会执行重新分配,即使这样,重新分配也只对整个结果发生一次,而不是每次连接都发生一次。