【问题标题】:Grouping consecutive identical items: IEnumerable<T> to IEnumerable<IEnumerable<T>>对连续相同的项目进行分组: IEnumerable<T> 到 IEnumerable<IEnumerable<T>>
【发布时间】:2010-05-13 15:49:01
【问题描述】:

我有一个有趣的问题:给定一个IEnumerable&lt;string&gt;,是否有可能产生一个IEnumerable&lt;IEnumerable&lt;string&gt;&gt; 序列,将相同的相邻字符串在一次传递中分组?

让我解释一下。

1.基本说明性示例:

考虑以下IEnumerable&lt;string&gt;(伪表示):

{"a","b","b","b","c","c","d"}

如何获得一个IEnumerable&lt;IEnumerable&lt;string&gt;&gt;,它会产生某种形式的东西:

{ // IEnumerable<IEnumerable<string>>
    {"a"},         // IEnumerable<string>
    {"b","b","b"}, // IEnumerable<string>
    {"c","c"},     // IEnumerable<string>
    {"d"}          // IEnumerable<string>
}

方法原型是:

public IEnumerable<IEnumerable<string>> Group(IEnumerable<string> items)
{
    // todo
}

但也可能是:

public void Group(IEnumerable<string> items, Action<IEnumerable<string>> action)
{
    // todo
}

...将为每个子序列调用action

2。更复杂的示例

好的,第一个示例非常简单,只是为了让高层意图清晰。

现在假设我们正在处理IEnumerable&lt;Anything&gt;,其中Anything 是这样定义的类型:

public class Anything
{
    public string Key {get;set;}
    public double Value {get;set;}
}

我们现在要根据 Key 生成子序列,(对具有相同 key 的每个连续的Anything 进行分组)以便以后使用它们来按组计算总值:

public void Compute(IEnumerable<Anything> items)
{
    Console.WriteLine(items.Sum(i=>i.Value));
}

// then somewhere, assuming the Group method 
// that returns an IEnumerable<IEnumerable<Anything>> actually exists:
foreach(var subsequence in Group(allItems))
{
    Compute(subsequence);
}

3.重要说明

  • 仅对原始序列一次迭代
  • 无中间集合分配(我们可以假设原始序列中有数百万个项目,每组中有数百万个连续项目)
  • 保留枚举器和延迟执行行为
  • 我们可以假设生成的子序列只会被迭代一次,并且会按顺序迭代。

有可能吗,你会怎么写?

【问题讨论】:

  • 我假设在您的示例答案中您的意思是 {"b", "b", "b"}
  • @Josh:很好——我解决了这个问题,谢谢!
  • 在您的复杂示例中, Sum 必须第二次迭代集合。如果调用代码将再次迭代相同的元素,那么将“Group”限制为一次迭代有什么意义?
  • 查看我的新答案。我认为它完成了你想要的!
  • @David B:想法是将原始列表的迭代委托给 n 个消费者。在这里,如果所有约束都得到遵守,Sum 方法将只迭代原始集合的一部分,该集合从未被迭代并且永远不会再次被迭代。实现这一目标的一种方法是透明地使用和共享一个枚举器。

标签: c# performance algorithm ienumerable


【解决方案1】:

这是你要找的吗?

  • 仅迭代列表一次。
  • 推迟执行。
  • 没有中间集合(我的另一篇文章不符合此标准)。

此解决方案依赖于对象状态,因为很难在两个使用 yield(无 ref 或 out 参数)的 IEnumerable 方法之间共享状态。

internal class Program
{
    static void Main(string[] args)
    {
        var result = new[] { "a", "b", "b", "b", "c", "c", "d" }.Partition();
        foreach (var r in result)
        {
            Console.WriteLine("Group".PadRight(16, '='));
            foreach (var s in r)
                Console.WriteLine(s);
        }
    }
}

internal static class PartitionExtension
{
    public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> src)
    {
        var grouper = new DuplicateGrouper<T>();
        return grouper.GroupByDuplicate(src);
    }
}

internal class DuplicateGrouper<T>
{
    T CurrentKey;
    IEnumerator<T> Itr;
    bool More;

    public IEnumerable<IEnumerable<T>> GroupByDuplicate(IEnumerable<T> src)
    {
        using(Itr = src.GetEnumerator())
        {
            More = Itr.MoveNext();

            while (More)
                yield return GetDuplicates();
        }
    }

    IEnumerable<T> GetDuplicates()
    {
        CurrentKey = Itr.Current;
        while (More && CurrentKey.Equals(Itr.Current))
        {
            yield return Itr.Current;
            More = Itr.MoveNext();
        }
    }
}

编辑:为更清洁的使用添加了扩展方法。固定循环测试逻辑,以便首先评估“更多”。

编辑:完成后处理枚举数

【讨论】:

  • 此解决方案无法处理枚举数。
  • 编辑做得很好。尽管如此,我发现这种实现仍然存在很大缺陷。如果您执行看似无害的myList = blah.Partition().ToList();,则会在无限循环中获得失控的内存消耗。
  • @Timwi - 是的,这太糟糕了。出现问题是因为分组逻辑依赖于列表内容的迭代,但是像 ToList() 这样的东西不会尝试展平 Enumerable,因此无法终止可枚举。我认为这可能是尝试在同一通道中分组和行动的固有限制。我想我会改变方法以满足 OP 的替代可接受解决方案,该解决方案将 Action 作为第二个参数。
  • 没关系.. 任何一种形式都有相同的限制,需要以“深度优先”的方式进行迭代。 :(如果你有解决这个问题的办法,我会很感兴趣的。谢谢你的仔细分析。
  • 正如 John Skeet 已经指出的那样,不可能满足 OP 的所有标准。您只能使用中间集合,如果连续组很大,这可能会占用大量内存,或者生成一个由原始序列和散布的“控制对象”组成的单个IEnumerable 来标记开始和结束每个子序列(作为抽象并不是非常有用)。
【解决方案2】:

满足所有要求的更好解决方案

好的,放弃我以前的解决方案(我将其留在下面,仅供参考)。这是我在发布初始帖子后想到的一种更好的方法。

编写一个实现IEnumerator&lt;T&gt; 并提供一些附加属性的新类:IsValidPrevious。这就是您真正需要解决的所有问题,因为您必须使用 yield 在迭代器块内维护状态。

我是这样做的(很简单,如您所见):

internal class ChipmunkEnumerator<T> : IEnumerator<T> {

    private readonly IEnumerator<T> _internal;
    private T _previous;
    private bool _isValid;

    public ChipmunkEnumerator(IEnumerator<T> e) {
        _internal = e;
        _isValid = false;
    }

    public bool IsValid {
        get { return _isValid; }
    }

    public T Previous {
        get { return _previous; }
    }

    public T Current {
        get { return _internal.Current; }
    }

    public bool MoveNext() {
        if (_isValid)
            _previous = _internal.Current;

        return (_isValid = _internal.MoveNext());
    }

    public void Dispose() {
        _internal.Dispose();
    }

    #region Explicit Interface Members

    object System.Collections.IEnumerator.Current {
        get { return Current; }
    }

    void System.Collections.IEnumerator.Reset() {
        _internal.Reset();
        _previous = default(T);
        _isValid = false;
    }

    #endregion

}

(我将其称为ChipmunkEnumerator,因为保持以前的值让我想起了花栗鼠的脸颊上有袋子来存放坚果。这真的重要吗?别再取笑我了。)

现在,在扩展方法中使用这个类来提供你想要的行为并不是那么难!

请注意,下面我将GroupConsecutive 定义为实际返回IEnumerable&lt;IGrouping&lt;TKey, T&gt;&gt;,原因很简单,如果无论如何按键分组,则返回IGrouping&lt;TKey, T&gt; 而不仅仅是IEnumerable&lt;T&gt; 是有意义的.事实证明,这无论如何都会对我们有所帮助......

public static IEnumerable<IGrouping<TKey, T>> GroupConsecutive<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
    where TKey : IEquatable<TKey> {

    using (var e = new ChipmunkEnumerator<T>(source.GetEnumerator())) {
        if (!e.MoveNext())
            yield break;

        while (e.IsValid) {
            yield return e.GetNextDuplicateGroup(keySelector);
        }
    }
}

public static IEnumerable<IGrouping<T, T>> GroupConsecutive<T>(this IEnumerable<T> source)
    where T : IEquatable<T> {

    return source.GroupConsecutive(x => x);
}

private static IGrouping<TKey, T> GetNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
    where TKey : IEquatable<TKey> {

    return new Grouping<TKey, T>(keySelector(e.Current), e.EnumerateNextDuplicateGroup(keySelector));
}

private static IEnumerable<T> EnumerateNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
    where TKey : IEquatable<TKey> {

    do {
        yield return e.Current;

    } while (e.MoveNext() && keySelector(e.Previous).Equals(keySelector(e.Current)));
}

(为了实现这些方法,我编写了一个简单的Grouping&lt;TKey, T&gt; 类,它以最直接的方式实现了IGrouping&lt;TKey, T&gt;。我省略了代码只是为了继续前进......)

好的,检查一下。我认为下面的代码示例很好地捕捉到了类似于您在更新后的问题中描述的更现实的场景。

var entries = new List<KeyValuePair<string, int>> {
    new KeyValuePair<string, int>( "Dan", 10 ),
    new KeyValuePair<string, int>( "Bill", 12 ),
    new KeyValuePair<string, int>( "Dan", 14 ),
    new KeyValuePair<string, int>( "Dan", 20 ),
    new KeyValuePair<string, int>( "John", 1 ),
    new KeyValuePair<string, int>( "John", 2 ),
    new KeyValuePair<string, int>( "Bill", 5 )
};

var dupeGroups = entries
    .GroupConsecutive(entry => entry.Key);

foreach (var dupeGroup in dupeGroups) {
    Console.WriteLine(
        "Key: {0} Sum: {1}",
        dupeGroup.Key.PadRight(5),
        dupeGroup.Select(entry => entry.Value).Sum()
    );
}

输出:

Key: Dan   Sum: 10
Key: Bill  Sum: 12
Key: Dan   Sum: 34
Key: John  Sum: 3
Key: Bill  Sum: 5

请注意,这也解决了我处理 IEnumerator&lt;T&gt; 值类型对象的原始答案的问题。 (用这种方法,没关系。)

如果您在这里尝试拨打ToList 仍然会出现问题,因为您会发现如果您尝试它。但考虑到您将延迟执行作为要求,我怀疑您是否会这样做。对于foreach,它可以工作。


原始、凌乱且有点愚蠢的解决方案

有些事情告诉我,我会因为这样说而遭到完全驳斥,但是......

是的,这是可能的(我认为)。请参阅下面的该死我拼凑的混乱解决方案。 (捕获异常以了解何时完成,因此您知道这是一个很棒的设计!)

现在,Jon 的观点是,如果您尝试执行 ToList,然后按索引访问结果列表中的值,则会出现一个非常实际的问题,这是完全有效的。但是,如果您的 only 意图是能够使用 foreach 循环 IEnumerable&lt;T&gt; - 而您 only 在您的 自己的代码——那么,我认为这对你有用。

不管怎样,这里有一个简单的例子来说明它是如何工作的:

var ints = new int[] { 1, 3, 3, 4, 4, 4, 5, 2, 3, 1, 6, 6, 6, 5, 7, 7, 8 };

var dupeGroups = ints.GroupConsecutiveDuplicates(EqualityComparer<int>.Default);

foreach (var dupeGroup in dupeGroups) {
    Console.WriteLine(
        "New dupe group: " +
        string.Join(", ", dupeGroup.Select(i => i.ToString()).ToArray())
    );
}

输出:

New dupe group: 1
New dupe group: 3, 3
New dupe group: 4, 4, 4
New dupe group: 5
New dupe group: 2
New dupe group: 3
New dupe group: 1
New dupe group: 6, 6, 6
New dupe group: 5
New dupe group: 7, 7
New dupe group: 8

现在是(乱七八糟的)代码:

请注意,由于这种方法需要在几个不同的方法之间传递实际的 枚举器,因此如果该枚举器是值类型,它将不起作用,就像对 @ 的调用987654344@ 在一种方法中只影响本地副本。

public static IEnumerable<IEnumerable<T>> GroupConsecutiveDuplicates<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer) {
    using (var e = source.GetEnumerator()) {
        if (e.GetType().IsValueType)
            throw new ArgumentException(
                "This method will not work on a value type enumerator."
            );

        // get the ball rolling
        if (!e.MoveNext()) {
            yield break;
        }

        IEnumerable<T> nextDuplicateGroup;

        while (e.FindMoreDuplicates(comparer, out nextDuplicateGroup)) {
            yield return nextDuplicateGroup;
        }
    }
}

private static bool FindMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer, out IEnumerable<T> duplicates) {
    duplicates = enumerator.GetMoreDuplicates(comparer);

    return duplicates != null;
}

private static IEnumerable<T> GetMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
    try {
        if (enumerator.Current != null)
            return enumerator.GetMoreDuplicatesInner(comparer);
        else
            return null;

    } catch (InvalidOperationException) {
        return null;
    }
}

private static IEnumerable<T> GetMoreDuplicatesInner<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
    while (enumerator.Current != null) {
        var current = enumerator.Current;
        yield return current;

        if (!enumerator.MoveNext())
            break;

        if (!comparer.Equals(current, enumerator.Current))
            break;
    }
}

【讨论】:

  • +1 用于改进使用。我对IsValidPrevious 有同样的认识。从使用的角度来看,您的解决方案比我的要好一些,但它使用相同的方法。
  • @dss539:很好,看起来像伟大的思想一样;)就个人而言,我确实喜欢拥有一个提供PreviousIsValid属性的IEnumerator&lt;T&gt;的想法,独立于任何特定问题,因为我觉得它在其他情况下也可以证明是有用的。但是你的方法肯定更简洁!
【解决方案3】:

你的第二个子弹是有问题的。原因如下:

var groups = CallMagicGetGroupsMethod().ToList();
foreach (string x in groups[3])
{
    ...
}
foreach (string x in groups[0])
{
    ...
}

在这里,它试图遍历第四组,然后是第一组......这显然只有在所有组都被缓冲时才能工作或者它可以重新读取序列,这两者都不是很理想。

我怀疑您想要一种更“反应性”的方法 - 我不知道 Reactive Extensions 是否满足您的要求(“连续”要求是不寻常的),但您基本上应该提供某种要执行的操作每个组...这样该方法就不必担心必须在阅读完之后返回一些可以使用的东西。

如果您希望我尝试在 Rx 中找到解决方案,或者您是否对以下内容感到满意,请告诉我:

void GroupConsecutive(IEnumerable<string> items,
                      Action<IEnumerable<string>> action)

【讨论】:

  • 我完全理解你的意思。但是,您可以认为我完全控制了调用代码,并且每个子序列只会按顺序迭代一次。 “提供要在每个组上执行的操作”——如何将组(作为 IEnumerable)传递给操作?
  • 这是一个很好的观点。我认为类似于 OP 试图做的事情,在精神上,可能的。他只需要了解它的局限性,例如,尝试像使用任何其他 IEnumerable 一样使用结果值(例如通过在其上调用 ToList)会导致问题。
  • @Romain: action(group); 当然是group is IEnumerable&lt;string&gt;。瞬间停电?
  • @Jon:我的意思是:如何让 IEnumerable 传递给操作?采取行动只能确保子序列按顺序消费,而且只消费一次,但我的问题更多是关于“如何在一次旅行中将可枚举划分为多个可枚举。
  • 虽然这里的 other Jon 有一个非常有效的观点,但我刚刚添加了一个答案。看看吧。
【解决方案4】:

这是一个我认为满足您的要求的解决方案,适用于任何类型的数据项,并且非常简短易读:

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list)
{
    var current = list.FirstOrDefault();

    while (!Equals(current, default(T))) {
        var cur = current;
        Func<T, bool> equalsCurrent = item => item.Equals(cur);
        yield return list.TakeWhile(equalsCurrent);
        list = list.SkipWhile(equalsCurrent);
        current = list.FirstOrDefault();
    }
}

注意事项:

  1. 存在延迟执行(TakeWhileSkipWhile 都这样做)。
  2. 我认为这只会对整个集合进行一次迭代(使用SkipWhile);当您处理返回的 IEnumerables 时,它会再次迭代集合,但分区本身只迭代一次。
  3. 如果您不关心值类型,可以添加约束并将while 条件更改为null 的测试。

如果我弄错了,我会对 cme​​t 指出错误特别感兴趣!

非常重要的一点:

此解决方案将不允许让您以任何顺序枚举生成的枚举,而不是它提供它们的顺序。但是,我认为原始海报在 cmets 中已经非常清楚,这不是一个问题。

【讨论】:

  • 有趣的方法,但你迭代整个列表两次。您确实将迭代分解为块,但每个项目都比较了两次(1 代表 Take,然后 1 代表 Skip)。此外,这排除了作为数据集一​​部分的默认值(例如空字符串或整数值 0)。不过,这很酷,我没有更好的方法。
  • @dss:嗯,任何解决方案显然都需要遍历集合一次以对其进行分区(这就是SkipWhile 在此处所做的)。第二次迭代仅在 you 迭代此方法提供的结果时发生(仅 thenTakeWhile 执行)。我错了吗?关于值类型:正如我所提到的,如果你想支持它们,这是可以做到的最好的。 :-)
  • 感谢乔恩的回答!这个解决方案似乎是正确的,但是关于第一个约束有一个小问题:使用 TakeWhile 然后 SkipWhile 使您在每个组上迭代 两次,因此您将集合迭代两次。
  • 您能看看我的新答案,如果您发现任何问题,请告诉我? stackoverflow.com/questions/2828203/…
  • 我认为您误认为任何解决方案都需要迭代两次。看看我的答案,它只迭代一次(你可以通过考虑整个解决方案只使用一个永远不会重置的枚举器来确认这一点)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-22
  • 1970-01-01
  • 1970-01-01
  • 2012-09-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多