【问题标题】:Working with list of tuples使用元组列表
【发布时间】:2012-12-30 22:48:37
【问题描述】:

我一直在尝试解决这个问题,但我就是想不通。所以,我有一个包含元组的列表,例如:

[("Mary", 10), ("John", 45), ("Bradley", 30), ("Mary", 15), ("John", 10)]

我想要得到的是一个包含元组的列表,如果名称相同,则应添加这些元组的编号,如果不是,则该元组也必须是最终列表的一部分,例如:

[("Mary",25), ("John", 55), ("Bradley", 30)]

我不知道我是否真的很好地解释了自己,但我想你可能会通过这些例子理解。

这个我试过了,还是不行:

test ((a,b):[]) = [(a,b)]
test ((a,b):(c,d):xs) | a == c = (a,b+d):test((a,b):xs)
                      | otherwise = (c,d):test((a,b):xs)

【问题讨论】:

  • this question 对你有帮助吗?
  • 你能用一堆上面有名字和数字的卡片手工解决吗?

标签: haskell tuples


【解决方案1】:

使用列表做这种事情总是很尴尬,因为它们具有顺序性——它们并不真正适合诸如“查找匹配项”或“通过组合列表元素的特定组合来计算新列表”之类的操作或其他本质上不连续的事物。

如果你退后一步,你真正想做的是,对于列表中每个不同的String,找到与之关联的所有数字并将它们相加。这听起来更适合键值样式数据结构,Haskell 中最标准的is found in Data.Map,它为您提供任何值类型和任何有序键类型的键值映射(即@987654325 的实例@)。

因此,要从您的列表中构建一个Map,您可以使用Data.Map... 中的fromList 函数...方便地,它期望以键值元组列表的形式输入。所以你可以这样做......

import qualified Data.Map as M

nameMap = M.fromList [("Mary", 10), ("John", 45), ("Bradley", 30), ("Mary", 15), ("John", 10)]

...但这不好,因为直接插入它们会覆盖数字而不是添加它们。在插入重复键时,您可以use M.fromListWith to specify how to combine values - 在一般情况下,通常使用它来为每个键或类似内容构建值列表。

但在您的情况下,我们可以直接跳到所需的结果:

nameMap = M.fromListWith (+) [("Mary", 10), ("John", 45), ("Bradley", 30), ("Mary", 15), ("John", 10)]

如果找到新名称,它将直接插入,否则它将在副本上添加值(数字)。如果您愿意,可以使用M.toList 将其转回元组列表:

namesList = M.toList $ M.fromListWith (+) [("Mary", 10), ("John", 45), ("Bradley", 30), ("Mary", 15), ("John", 10)]

这给了我们[("Bradley",30),("John",55),("Mary",25)]的最终结果。

但是,如果您想对名称/数字的集合做更多的事情,在完成之前将其保留为 Map 可能更有意义。

【讨论】:

  • 打我一拳。很高兴看到我正在编写相同的解决方案:)
  • @Tinctorius:是的,你很难找到更直接的“在标准库中使用这个东西”的例子!我打算在评论中链接到Data.Map,但我认为使用fromListWith 需要更多说明。
  • import qualified 是做什么的?
  • @Marcin:导入合格的名称——注意我写了M.fromListWith之类的函数。您通常希望导入合格的Data.Map,因为它定义了一堆与Prelude 函数同名的函数——仅使用import Data.Map,您将得到各种不明确的名称错误。实际上,如果您查看 Data.Map 的文档页面,它会明确建议导入合格的。
  • @Raphm 如果您首先sort 列表,然后与相同的String 组件相邻,则您可以仅使用列表来获得不太复杂的实现。然后你可以做一些与你的尝试类似的事情。
【解决方案2】:

这是使用列表的另一种方式:

import Data.List

answer :: [(String, Int)] -> [(String, Int)]
answer = map (foo . unzip) . groupBy (\x y -> fst x == fst y) . sort            
   where foo (names, vals) = (head names, sum vals)

这是一种相当简单的方法。 首先,点(.) 表示函数组合,它允许我们将值从一个函数传递到下一个函数,即一个函数的输出成为下一个函数的输入,以此类推。我们首先应用sort,它将自动将列表中的名称彼此相邻移动。接下来,我们使用groupBy 将具有相似名称的每一对放入一个列表中。我们最终得到一个列表列表,每个列表都包含具有相似名称的对:

[[("Bradley",30)], [("John",10),("John",45)], [("Mary",10),("Mary", 15)]]

给定这样一个列表,您将如何处理每个子列表? 也就是说,您将如何处理包含所有相同名称的列表?

显然我们希望将它们缩小为一对,其中包含名称和值的总和。为此,我选择了函数(foo . unzip),但还有很多其他方法可以实现。 unzip 获取一个配对列表并创建一个配对。该对包含 2 个列表,第一个包含所有名称,第二个包含所有值。这对然后通过函数组合传递给foo,如前所述。 foo 使用模式将其分开,然后将head 应用于名称,仅返回一个名称(它们都相同),并将sum 应用于值列表。 sum 是另一个标准的列表函数,自然地对列表中的值求和。

但是,这个(foo . unzip) 仅适用于单个对列表,但我们有一个列表列表。这就是map 的用武之地。map 会将我们的(foo . unzip) 函数应用于列表中的每个列表,或者更一般地说,应用于列表中的每个元素。我们最终得到一个列表,其中包含将(foo . unzip) 应用于每个子列表的结果。

我建议查看Data.List 中使用的所有列表函数。

【讨论】:

  • @Raphm 我在上面的答案中添加了更多内容。希望这会有所帮助。
  • @Raphm 抱歉,我应该第一次尝试解释。
  • 为了教学:我能问一下使用Data.Map有什么不直接和简单的吗?
  • @Tinctorius 什么都没有,但很明显他想要一个仅限于列表的解决方案。我只是把他当作一个学生,负责理解常见的列表函数,使用 Data.Map,虽然是一个聪明的解决方案,但有点无关紧要。
【解决方案3】:

我认为您的潜在解决方案不起作用的原因是,如果它们以列表中的相同键顺序出现,它只会将元素组合在一起。因此,我将使用映射(如果您使用过其他语言,通常称为字典)来记住我们看到的键并保留总数。首先我们需要导入我们需要的函数。

import Data.Map hiding (foldl, foldl', foldr)
import Data.List (foldl')

现在我们可以沿着列表折叠,并为每个键值对相应地更新我们的地图。

sumGroups :: (Ord k, Num n) => [(k, n)] -> Map k n
sumGroups list = foldl' (\m (k, n) -> alter (Just . maybe n (+ n)) k m) empty list

所以,foldl' 使用函数遍历列表。它使用每个元素(此处为 (k, n) 对)和另一个参数累加器调用该函数。这是我们的地图,一开始是空的。对于每个元素,我们使用 Maybe n -> Maybe n 的函数来更改映射。这反映了地图可能在键 k 下可能没有任何内容的事实 - 所以我们处理这两种情况。如果没有前一个值,我们只返回 n,否则我们将 n 添加到前一个值。这给了我们一个最后的地图,其中应该包含组的总和。对结果调用 toList 函数应该会给你想要的列表。

在 ghci 中测试这个结果:

 $ ghci
GHCi, version 7.6.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> import Data.Map hiding (foldl, foldl', foldr)
Prelude Data.Map> import Data.List (foldl')
Prelude Data.Map Data.List> let sumGroups list = foldl' (\m (k, n) -> alter (Just . maybe n (+ n)) k m) empty list
Loading package array-0.4.0.1 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package containers-0.5.0.0 ... linking ... done.
Prelude Data.Map Data.List> toList $ sumGroups $ [("Mary", 10), ("John", 45), ("Bradley", 30), ("Mary", 15), ("John", 10)]
[("Bradley",30),("John",55),("Mary",25)]
Prelude Data.Map Data.List> 

作为奖励,这些组按排序顺序出现,因为内部 map 使用一种二叉树的形式,因此按顺序遍历并输出排序(好吧,无论如何是按键排序)列表相对简单。

【讨论】:

  • 我确信不使用 Data.Map 也是可能的,但我倾向于认为不使用 Data.Map 只会让它变得更加复杂。如果您首先对列表进行排序,那么您的原始方法会起作用,但我倾向于说您的原始方法更复杂,并且您最终会一直处理棘手的情况。另外,尽管它们具有相同的大 O 复杂度,但以这种方式收集它们意味着可能会延迟生成列表,这对于具有许多相同键的长列表在内存使用方面可能是一个优势。
【解决方案4】:

这是我的两分钱。仅使用 Haskell Prelude。

test tup = sumAll
  where
    collect ys [] = ys
    collect ys (x:xs) =
        if (fst x) `notElem` ys
        then collect (fst x : ys) xs
        else collect ys xs
    collectAllNames = collect [] tup

    sumOne [] n x = (x, n)
    sumOne (y:ys) n x =
        if fst y == x
        then sumOne ys (n + snd y) x
        else sumOne ys n x

    sumAll = map (sumOne tup 0) collectAllNames

该方法多次遍历原始列表。 Collect 构建一个仅包含名称的临时列表,跳过名称重复。 sumOne 取一个名称,检查列表中匹配的名称,然后添加它们的数字。它返回名称以及总和。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-27
    • 2018-03-02
    • 2012-10-31
    • 1970-01-01
    • 1970-01-01
    • 2013-05-29
    相关资源
    最近更新 更多