【问题标题】:Analog of python groupby in haskellhaskell中python groupby的模拟
【发布时间】:2017-04-10 04:51:10
【问题描述】:

在python中有groupby函数。

它的类型可以像这样用haskell表示groupby :: a->b->[a]->[(b, [a])] 因为它需要对数据进行排序,我们可以将其运行时间视为O(n*log(n))

我可能不是唯一对此不满意的人,所以我找到了这个library groupby 的这种实现需要对输入序列进行两次传递。所以我认为它的运行时间是O(n),但正如它在文档中所说的那样,它并不是真的懒惰,因为如果你不将密钥传递给它,它需要进行传递序列以从项目中收集所有唯一密钥.

所以我想,引用雷蒙德·赫廷格的话

一定有更好的办法!

所以我写了这个

from collections import defaultdict, deque


def groupby(sequence, key=lambda x: x):
    buffers = defaultdict(deque)
    kvs = ((key(item), item) for item in sequence)
    seen_keys = set()
    def subseq(k):
        while True:
            buffered = buffers[k]
            if buffered:
                yield buffered.popleft()
            else:
                next_key, value = next(kvs)
                buffers[next_key].append(value)
    while True:
        try:
            k, value = next(kvs)
        except StopIteration:
            for bk, group in buffers.items():
                if group and bk not in seen_keys:
                    yield (bk, group)
            raise StopIteration()
        else:
            buffers[k].append(value)
        if k not in seen_keys:
            seen_keys.add(k)
            yield k, subseq(k)

如果您不熟悉 python,这个想法很简单。 创建key -> queue of elements的可变字典 尝试获取序列的下一个元素及其键值。 如果序列不为空,则根据其键将此值添加到组队列中。如果我们没有看到这个键产生一对 (key, iterable group),后者会从缓冲区或序列中获取键。如果我们已经看到这个,这个键什么也不做,然后循环。

如果序列结束,则意味着它的所有元素都已经放入缓冲区(并且可能已被消耗)。如果缓冲区不为空,我们将对其进行迭代并产生重命名(键,可迭代)对。

我已经对它及其工作进行了单元测试。而且它真的很懒(意味着它不会从序列中获取任何价值,直到消费者没有要求它),它的运行时间应该是O(n)

我试过用haskell模拟这个函数,但没有找到。

可以在haskell中写这样的东西吗?如果是,请给出解决方案,如果不是,请解释原因。

【问题讨论】:

  • @leftaroundabout 是的,基本一样,但是类型是a->b->[[a]]。我怎么知道哪个等价类是哪个?你看,我在 hoogle 上搜索了a->b->[(b, [a])]
  • @leftaroundabout 另一方面,我可能会尝试阅读源代码并弄清楚如何更改它,以便它返回等价类的名称。我浏览了来源,从进口判断它使用可变状态,对吗?你认为没有可变状态这是可能的吗?
  • 显然[(b, [a])] 类型不是你想要的——Haskell 链表不是 python 字典!正如您在下面的答案中看到的那样,您根本无法获得您所寻求的性能。你的 python 函数消耗并产生一个列表并不重要——它在内部使用可变性,你的 Haskell 函数也必须这样做——如果你完全在 ST 中工作,你仍然可以在最后产生一个纯值。
  • @user1685095 如果你想输入[(b, [a])],你可以只映射结果列表以将[a]转换为(b, [a])。只需使用map (\l -> (key $ head l, l))

标签: python haskell iterator lazy-sequences


【解决方案1】:

如果我理解正确的话,你想要的类型是

(a -> k) -> [a] -> [(k, [a])]

即给定一个键函数和一个项目列表,按键对项目进行分组。

在 Haskell 中有一个库函数 groupBy 可以做类似的事情。它假定您有一个排序列表,并将满足布尔条件的项目分组到子列表中。我们可以用它来做你想做的事:

import Data.List
import Data.Ord

groupByKey :: (a -> k) -> [a] -> [(k, [a])]
groupByKey keyF xs = map getResult groups
   where
      keyPairs = map (\v -> (keyF v, v)) xs
      groups = groupBy (\v1 v2 -> fst v1 == fst v2) 
                  $ sortBy (comparing fst) keyPairs
      getResult xs = (fst $ head xs, map snd xs)

keyPairs 是参数中每个元素的 (key, value) 对。 groups 首先使用sortBy 将其排序为键顺序,然后将结果分组到共享相同键的子列表中。 getResult 将子列表转换为包含键(取自头元素)和原始值列表的对。 我们可以安全地使用head,因为groupBy 从不提供空子列表。

【讨论】:

  • 嗯,这是显而易见的解决方案,但它的运行时间是O(n*log(n))。也许这还不够清楚,但我想要一个懒惰且具有O(n) 运行时间的解决方案。
  • 鉴于需要按键顺序对元素进行排序,我不知道如何获得它。也许我误解了你想要什么。我可以看到使用键表会给你 O(n log k)。是这样吗?
  • 好吧,你看到我在 python 中是如何做到的了吗?我的实现没有指定要发出的键顺序,但可以修改它以按特定顺序输出对。关键是元素的缓冲。还有来自@leftaroundabout 的有用链接。写歧视包的人已经基本做到了,所以在haskell中也是可能的。
  • 实际上与GHC捆绑的排序功能使用运行识别,所以我很确定我的版本也是O(n log k)。
  • k 究竟是什么?仍然O(n*log k) 不是O(n)
猜你喜欢
  • 2010-11-20
  • 2013-03-11
  • 1970-01-01
  • 1970-01-01
  • 2017-01-11
  • 1970-01-01
  • 1970-01-01
  • 2013-05-27
相关资源
最近更新 更多