簡體   English   中英

對連續的相同項進行分組:IEnumerable <T> 到IEnumerable <IEnumerable<T> &gt;

[英]Grouping consecutive identical items: IEnumerable<T> to IEnumerable<IEnumerable<T>>

我有一個有趣的問題:給定一個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;}
}

現在,我們要基於密鑰的子序列,(組每個連續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.重要說明

  • 只對原始序列進行一次迭代
  • 沒有中間收集分配(我們可以假設原始序列中有數百萬個項目,每組中有數百萬個連續項目)
  • 保持調查員和延遲執行行為
  • 我們可以假設結果子序列只迭代一次,並將按順序迭代。

它有可能嗎,你會怎么寫呢?

這是你想要的?

  • 僅迭代列表一次。
  • 推遲執行。
  • 沒有中間收藏(我的其他帖子在此標准上失敗)。

此解決方案依賴於對象狀態,因為很難在使用yield(無ref或out params)的兩個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();
        }
    }
}

編輯:添加了清潔用法的擴展方法。 固定循環測試邏輯,以便首先評估“更多”。

編輯:完成后處理枚舉器

滿足所有要求的更好解決方案

好的,廢棄我之前的解決方案(我將其留在下面,僅供參考)。 這是我初次發布后發生的更好的方法。

編寫一個實現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 ,然后通過索引訪問結果列表中的值,則完全有效。 但是如果你在這里的唯一目的是能夠使用foreach IEnumerable<T> - 並且你只是自己的代碼中執行此操作 - 那么,我認為這可能適合你。

無論如何,這是一個如何工作的快速示例:

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

現在為(麻煩的廢話)代碼:

請注意,由於此方法需要在幾個不同的方法之間傳遞實際的枚舉器 ,因此如果該枚舉器是值類型則不起作用 ,因為在一個方法中對MoveNext僅影響本地副本。

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

你的第二顆子彈是有問題的。 原因如下:

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)

這是一個我認為滿足您的要求的解決方案,適用於任何類型的數據項,並且非常簡短和可讀:

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測試。

如果我在某種程度上錯了,我會特別感興趣的是指出錯誤的評論!

非常重要的旁邊:

該解決方案將不允許枚舉產生可枚舉比它提供他們在一個以外的任何命令。不過,我認為原來的海報已在意見很清楚,這是沒有問題的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM