对连续相同的项进行分组:IEnumerable <t>到IEnumerable <ienumerable <t>&gt; </ienumerable <t> </t>

时间:2010-05-13 15:49:02

标签: c# performance algorithm ienumerable

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

让我解释一下。

1。基本说明性示例:

考虑以下IEnumerable<string>(伪表示):

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

如何获得会产生某种形式的IEnumerable<IEnumerable<string>>

{ // 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<Anything>,其中Anything是这样定义的类型:

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

我们现在想要根据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。重要说明

  • 一次迭代超过原始序列
  • 没有中间收藏分配(我们可以假设原始序列中有数百万个项目,每组中有数百万个连续项目)
  • 保持调查员和延迟执行行为
  • 我们可以假设结果子序列只会迭代一次,并且会按顺序迭代。

有可能,你会怎么写呢?

4 个答案:

答案 0 :(得分:5)

这是你在找什么?

  • 仅列出一次列表。
  • 推迟执行。
  • 没有中间收藏(我的其他帖子在此标准上失败)。

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

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();
        }
    }
}

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

编辑:完成后处理枚举器

答案 1 :(得分:3)

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

好的,废弃我之前的解决方案(我将其留在下面,仅供参考)。这是我在初次发帖后发生的更好的方法。

编写一个实现IEnumerator<T>的新类,并提供一些其他属性: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<IGrouping<TKey, T>>,原因很简单,如果这些按键分组,则返回IGrouping<TKey, T>更有意义而不仅仅是IEnumerable<T>。事实证明,无论如何这将帮助我们......

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<TKey, T>类,以最简单的方式实现IGrouping<TKey, T>。我省略了代码,以便继续前进......)

好的,看看吧。我认为下面的代码示例很好地捕获了一些类似于您在更新的问题中描述的更真实的场景。

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<T>对象的原始答案的问题。 (用这种方法,没关系。)

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


原创,凌乱,有些愚蠢的解决方案

有些东西告诉我,我会因为这样说完全被驳斥,但是......

,有可能(我认为)。请参阅下面的一个该死的凌乱的解决方案。 (抓住一个例外,知道什么时候结束,所以你知道这是一个很棒的设计!)

现在,Jon指出,如果您尝试执行此操作(例如ToList,然后按索引访问结果列表中的值),则存在一个非常实际的问题,这是完全有效的。但是,如果您的意图是使用IEnumerable<T>来覆盖foreach - 而您只是 这样做你的自己的代码 - 那么,我认为这对你有用。

无论如何,这是一个如何运作的简单例子:

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

现在为(垃圾般的废话)代码:

请注意,由于此方法需要在几个不同的方法之间传递实际的枚举器,如果该枚举数是值类型,则将无效,因为对{的调用{1}}在一种方法中只影响本地副本。

MoveNext

答案 2 :(得分:2)

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

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)

答案 3 :(得分:2)

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

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的测试。
  4. 如果我在某种程度上错了,我会对指出错误的评论特别感兴趣!

    非常重要:

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