【问题标题】:How to find a previous value to a given x in trees in Haskell?如何在 Haskell 中的树中找到给定 x 的先前值?
【发布时间】:2015-01-25 20:12:28
【问题描述】:

任务是在 Haskell 的树中找到给定 x 的前一个值。重要的!不允许在任何部分使用列表:

data SearchTree a = Leaf a | Node a (SearchTree a) (SearchTree a)

t1 = Node 5 ( Node 4 ( Leaf 1) 
                     ( Node 5 (Leaf 4) (Leaf 5))
            ) 
            ( Node 7 ( Node 6  ( Leaf 5 ) ( Leaf 7  )) 
                     ( Node 10 ( Leaf 9 ) ( Leaf 11 )) 
            )

previousVal :: Ord a => a -> (SearchTree a) -> a
previousVal x t = previousValHelp x t 

previousValHelp x (Leaf n) = error "Es gibt nichts kleineres"
previousValHelp x (Node n l r) = (max n (max (previousValHelp x l) (previousValHelp x r))) < x 

例如previousVal 9 t1 应该给7。 我设法制作了这段代码,但它是错误的......这里有人有想法吗?

谢谢!

【问题讨论】:

  • 最近有很多关于二叉搜索树的问题。这是讲座的一部分吗?顺便说一句,previousVal :: Ord a =&gt; a -&gt; SearchTree a -&gt; Maybe a 会更安全一些。
  • 恐怕我不太明白你的问题。你能解释一下为什么它应该返回7 给定9t1 并输入?另外,我真的没有看到制作previousValHelp 函数的意义。 previousVal 只是直接调用它,不做任何其他事情。为什么不按照 previousValHelp 的方式定义 previousVal
  • 你认为previousVal 5 t1应该是什么? previousVal 8 t1 呢?
  • 我建议您阅读有关zippers 的内容,然后将其概括为在给定元素之前或之后查找元素n

标签: list haskell tree


【解决方案1】:

Haskell 是关于类型的。所以最好从函数的类型签名而不是定义开始。让类型检查器告诉你出了什么问题也很有意义。它通常会导致解决方案。

所以,我尝试编译您的代码。编译器输出有时可能会有点混乱,但这是每个人都必须经历的事情。我通过将previousVal x t = previousValHelp x t 更改为previousVal = previousValHelp(称为eta reduction,但如果它让您感到困惑,您可以忽略它)来使输出更有用。

这是输出:

SearchTree.hs:11:15:
    Could not deduce (a ~ Bool)
    from the context (Ord a)
      bound by the type signature for
                 previousVal :: Ord a => a -> SearchTree a -> a
      at SearchTree.hs:10:16-48
      ‘a’ is a rigid type variable bound by
          the type signature for
            previousVal :: Ord a => a -> SearchTree a -> a
          at SearchTree.hs:10:16
    Expected type: a -> SearchTree a -> a
      Actual type: Bool -> SearchTree Bool -> Bool
    Relevant bindings include
      previousVal :: a -> SearchTree a -> a (bound at SearchTree.hs:11:1)
    In the expression: previousValHelp
    In an equation for ‘previousVal’: previousVal = previousValHelp

看看它是如何告诉你它期望的 previousValHelp 的类型,但它却得到了什么:

Expected type: a -> SearchTree a -> a
  Actual type: Bool -> SearchTree Bool -> Bool

这种类型的错误也显示了逻辑错误。 previousValHelp 的定义基本上是foo &lt; bar(只需忽略括号和递归)。 请注意,上述两行代码不会出现在您的初始代码版本中......所以试着弄清楚整个编译器输出的前几行的含义。 p>

不管怎样,现在让我们检查一下(&lt;) 在ghci 中的类型(&lt;(&lt;)中缀 版本):

Prelude> :t (<)
(<) :: Ord a => a -> a -> Bool

所以它返回一个 Bool 类型的值(如果给定这两个 a 参数)。有道理。

记住我是如何告诉你暂时忽略递归和括号的。现在让我们弄清楚为什么不仅函数的“返回类型”是 Bool,而且其余的也是。让我们看看:

previousValHelp x (Node n l r) =
  max n (max (previousValHelp x l) (previousValHelp x r)) < x

这可能有点棘手。由于我们已经知道previousValHelp 是一个在给定两个参数时返回 Bool 的函数,我们可以推断出递归调用 (previousValHelp x l)(previousValHelp x r) 也返回一个 Bool。这意味着我们正在运行带有两个 Bool 参数的内部函数 max (previousValHelp x l) (previousValHelp x r) 并返回一个 Bool。查看类型签名:

max :: Ord a => a -> a -> a

现在我们也有了外部函数max n (max (...) (...))...我们已经知道它的第二个参数的类型是Bool(见上文)。由于类型必须一致(max True True 有效,但 max True 1 无效)第一个参数 n 也被假定为 Bool。啊哈。检查您对 SearchTree 的定义...n 是 SearchTree 的类型参数,因此我们得到 SearchTree Bool! 同样,(&lt;) 的参数也必须一致。 True &lt; False 有效,而 True &lt; 1 无效。 所以最后一步是找出 x 的类型。我们已经知道&lt;的左边参数是Bool...所以x也必须是Bool,这是我们的第一个函数参数。

我的猜测是,您可能希望使用(&lt;) 的方式让您可以选择要执行的操作...而不是只返回结果。 为此,有guards

再次重申:对于像 haskell 这样高度关注类型的语言......学习如何阅读类型/编译器错误非常重要。

【讨论】:

    【解决方案2】:

    注意:在下文中,我假设“x 的前一个值”表示“树中小于 x 的最大值”并且 SearchTree 是二叉搜索树(即已排序)。

    让我们先看看为什么你的代码没有按照你想要的方式运行。

    Haskell 提供给我们的一大好处是表达类型以及类型推断,所以让我们看看它认为你的 previousValHelp 函数有什么类型:

    Prelude> :t previousValHelp
    previousValHelp :: Bool -> SearchTree Bool -> Bool
    

    啊!有问题!我们对previousValHelp 的实现仅适用于SearchTree Bool!但是,这是不对的,我们希望它适用于所有符合约束 Ord aa。尝试找出原因。 (我不打算在这里解释它,但尝试通过类型来了解为什么SearchTree 必须是SearchTree Bool。)

    让我们开始重写我们的previousVal 函数。我们将从类型签名开始:

    previousVal :: Ord a => a -> (SearchTree a) -> Maybe a
    

    这表示“假设我们可以比较某种类型的对象 a,我们将定义一个函数,它接受一个 a 和一个包含 as 的 SearchTree,然后返回给我们一个a(包裹在Just中)或Nothing"。 (我们将使用Maybe a 而不是error,因为这样我们可以使用类型系统通过强制我们显式处理Nothing 结果来确保我们的程序不会崩溃。它还使递归函数的实现要容易得多。)

    现在,我们知道 SearchTree 有两种可能的“形状”:NodeLeaf。我们必须为他们两个都实施一个案例。

    previousVal x (Node v l r) = _
    previousVal x (Leaf v) = _
    

    让我们从Node 案例开始。这里有两种可能的情况:如果这个Node处的值小于我们要找的值,那么我们要走右子树(r),否则我们要走左子树(l)。

    previousVal x (Node v l r) | v < x = previousVal x r
                               | otherwise = previousVal x l
    

    对于Leafs,逻辑更简单:如果Leaf 的值小于我们正在寻找的值,那么我们可以返回该值(因为在我们下面没有其他东西可以“更接近“比我们目前所处的位置)。否则,我们只返回Nothing

    previousVal x (Leaf v) | v < x = Just v
                           | otherwise = Nothing
    

    现在,如果您运行到目前为止我们编写的代码,您应该会发现它在某些情况下有效,在某些情况下会失败。例如:

    Prelude> previousVal 5 t1
    Just 4
    Prelude> previousVal 9 t1
    Nothing
    

    为什么有时它似乎有效,但有时却无效?好吧,事实证明我们在上面的Node 案例中实际上有一个错误。看,当我们沿着右侧分支向下时,该子树中可能不会有任何值小于x。在这种情况下,右手边的树会返回一个Nothing 给我们。但是如果右侧子树返回Nothing,那么我们当前所在的Node 包含小于x 的最大值,所以我们应该返回Just v。而不是上面的Node 案例,我们应该有这样的东西:

    previousVal x (Node v l r) | v < x = case previousVal x r of
                                           Nothing -> Just v
                                           value -> value
                               | otherwise = previousVal x l
    

    现在它也应该适用于 9:

    Prelude> previousVal 9 t1
    Just 7
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-10
      • 2021-09-07
      • 1970-01-01
      • 1970-01-01
      • 2023-01-14
      • 1970-01-01
      相关资源
      最近更新 更多