2

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

[("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)
4

4 回答 4

8

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

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

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

import qualified Data.Map as M

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

...但这不好,因为直接插入它们会覆盖数字而不是添加它们。您可以使用M.fromListWith指定在插入重复键时如何组合值- 在一般情况下,通常使用它来为每个键或类似事物构建值列表。

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

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)]

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

于 2012-12-30T23:09:33.967 回答
4

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

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)]]

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

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

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

我建议查看Data.List.

于 2012-12-31T00:25:23.230 回答
1

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

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 使用一种二叉树形式,因此按顺序遍历并输出排序(好吧,无论如何按键排序)列表相对简单。

于 2012-12-30T23:14:36.587 回答
0

这是我的两分钱。仅使用 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 取一个名称,检查列表中匹配的名称,然后添加它们的数字。它返回名称和总和。

于 2012-12-31T12:10:27.640 回答