【问题标题】:Element is in first half of the list, Haskell元素在列表的前半部分,Haskell
【发布时间】:2021-12-26 02:24:07
【问题描述】:

阅读 Get Programming with Haskell 一书,其中一个问题是找出给定元素是否在列表的前半部分。这可以这样做

isInFirstHalf x xs = elem x firstHalf
    where firstHalf = take (length xs `div` 2) xs

但是,问题是这里length 遍历了整个列表。在命令式语言中,可以通过跟踪元素索引和当前计数器来提前缩短循环。例如。如果列表有一百万个元素,并且第三个元素匹配,一旦你完成了第六个元素的循环,你可以立即返回 true。

我的问题是是否有办法在 Haskell 中实现类似的功能。

【问题讨论】:

  • 您可以肯定地跟踪索引,但是您如何知道索引是否在上半年结束?你仍然需要长度。大多数命令式语言的不同之处在于它们中的大多数使用数组,而不是列表(即使他们称它们为列表),其中长度为 O(1)。你猜怎么着? Haskell 也有数组!
  • @FyodorSoikin 你有没有错过 Q 中的这一部分,“例如,如果列表有一百万个元素,并且第三个元素有匹配,一旦你完成了第六个元素的循环,你可以立即返回 true”? :)
  • 啊,是的,我完全做到了

标签: haskell


【解决方案1】:

当然。

halfAsLong (x:_:xs) = x:halfAsLong xs
halfAsLong _ = []

isInFirstHalf x xs = elem x (zipWith const xs (halfAsLong xs))

试试看:

> isInFirstHalf 3 (1:2:3:4:5:6:undefined)
True

读者练习:

  • 您提出的命令式解决方案的元素索引和当前计数器去了哪里? (它们还在那里,只是以某种我认为有点微妙的方式隐藏起来!)
  • 将奇数长度分成两半时会向下取整,就像length xs `div` 2 所做的那样。代码必须如何更改才能像 (length xs + 1) `div` 2 那样进行四舍五入?

【讨论】:

    【解决方案2】:

    Daniel Wagner 发布了一个非常好的答案,表明您实际上并不需要索引。

    不过,如果您确实想使用索引,可以按如下方式制定解决方案。

    • 我们通过将列表元素与其索引配对来枚举所有列表元素。这是通过使用zip [0..] xs(或zip [1..] xs,如果您想从 1 开始计数)来完成的。
    • 我们查找您的x 是否在列表中,如果存在则查找其索引i。可以通过直接递归进行,或者使用dropWhile ((/= x) . fst) ... 之类的东西,然后测试结果。
    • 一旦我们知道i,我们需要检查之后是否至少有i元素。这可以通过直接递归解决,也可以通过dropping i-1 元素并测试结果是否为非空列表来解决。

    当然,还有其他选择。例如,我们可以使用 zip [0..] 跳过枚举元素,并通过跟踪当前索引来使用递归:foo n x (y:ys) = ... foo (n+1) x ys ...

    【讨论】:

    • 这是不对的。 :( Q 特别要求短路行为。
    • @WillNess 我认为这只会根据需要检查列表,实现所需的短路。我错过了什么我看不到的东西吗?
    • 我的意思是如果x 在上半场不存在,或者根本不存在。或者也许我错过了一些东西......是的,我是。 :) 有趣...
    • 如果== 比较是一个重要的、繁重的计算,那么唯一反对这种方法的事情仍然存在。所以,是的,它仍然做的工作可能比需要的要多。
    • @WillNess 我现在认为你是对的:如果x 没有找到,Daniel 的解决方案可以提前找到[],避免在上半场之后使用==。我的解决方案将在所有列表中使用==
    【解决方案3】:

    这是思考任务的另一种方式。一个元素x 出现在列表xs 的前半部分,不包括中点,如果元素第一次出现之前的元素比它之后的少。

    我们可以编写break (== x) xs,使用标准函数break :: (a -> Bool) -> [a] -> ([a], [a])xs分成两部分:出现在x之前的部分(或所有xs,如果找不到x),以及余数(包括x,如果找到的话)。

    > break (== 0) []
    ([], [])
    
    > break (== 0) [0, 1]
    ([], [0, 1])
    
    > break (== 0) [1, 0]
    ([1], [0])
    
    > break (== 0) [1, 2, 0, 3, 4]
    ([1, 2], [0, 3, 4])
    
    > break (== 0) [1, 2, 3, 4]
    ([1, 2, 3, 4], [])
    

    然后我们想比较这两个部分的长度,而不像Int那样严格计算实际长度。为此,我们可以通过忽略其所有元素来计算每个部分的形状,使用专门用于列表的shape = map (const ()),又名void :: (Functor f) => f a -> f ()

    shape :: [a] -> [()]
    shape = void
    

    列表的Ord 实例按字典顺序对它们进行排序,所有() 类型的值都是相等的——好吧,() 类型的 only 值——所以形状比较@987654338 @ 是列表长度的比较,这对于我们的目的来说也足够懒惰。 (对于最大的惰性,shape 可以定义为 genericLength 在惰性自然数类型上,例如 data N = Z | S N 并带有适当的 Ord 实例。)

    > [] < repeat ()
    True
    
    > shape [5 .. 10] >= shape [1 .. 3]
    True
    
    > shape [1 .. 3] > shape [1 ..]
    False
    

    我们还可以使用drop 1“减少”列表的形状,如果找到它,我们将使用它来跳过计算元素本身。 (或者,我们可以使用(() :)“增加”形状。)

    最后,将这些元素放在一起会产生一个相当简单的解决方案:

    isInFirstHalf :: (Eq a) => a -> [a] -> Bool
    isInFirstHalf x xs = shape before < shape (drop 1 after)
      where
        (before, after) = break (== x) xs
    

    注意如果没有找到元素,after 会是空的,所以drop 1 将不起作用,但是before 的形状不可能小于空的形状[],所以比较 (&lt;) 仍将正确返回 False

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-05
      • 2017-12-14
      • 2022-10-09
      • 2020-09-21
      相关资源
      最近更新 更多