以递增的顺序将列表拆分为多个列表

Posted

技术标签:

【中文标题】以递增的顺序将列表拆分为多个列表【英文标题】:Split a list into multiple lists at increasing sequence broken 【发布时间】:2016-09-01 10:15:42 【问题描述】:

我有一个 int 列表,我想在找到较低或相同的数字时拆分原始列表后创建多个列表。 数字未按顺序排列。

 List<int> data = new List<int>  1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;

我希望结果如下:

  1, 2 
  1, 2, 3 
  3 
  1, 2, 3, 4 
  1, 2, 3, 4, 5, 6 

目前,我正在使用以下 linq 来执行此操作,但没有帮助我:

List<int> data = new List<int>  1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;
List<List<int>> resultLists = new List<List<int>>();
var res = data.Where((p, i) =>

    int count = 0;
    resultLists.Add(new List<int>());
    if (p < data[(i + 1) >= data.Count ? i - 1 : i + 1])
    
        resultLists[count].Add(p);
    
    else
    
        count++;
        resultLists.Add(new List<int>());
    
    return true;
).ToList();

【问题讨论】:

在 LinQ 中包含 count 变量,这不会导致所有内容始终添加到第 0 个列表中吗? 是否像您的示例中那样连续增加数字,或者它们可能有差距?当数字连续时,存在一个非常好的解决方案。 只是为了说明。数字没有必要有任何排序顺序。 【参考方案1】:

我只想做一些简单的事情:

public static IEnumerable<List<int>> SplitWhenNotIncreasing(List<int> numbers)

    for (int i = 1, start = 0; i <= numbers.Count; ++i)
    
        if (i != numbers.Count && numbers[i] > numbers[i - 1])
            continue;

        yield return numbers.GetRange(start, i - start);
        start = i;
    

你会这样使用:

List<int> data = new List<int>  1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;

foreach (var subset in SplitWhenNotIncreasing(data))
    Console.WriteLine(string.Join(", ", subset));

如果你确实需要使用IEnumerable&lt;T&gt;,那么我能想到的最简单的方法是这样的:

public sealed class IncreasingSubsetFinder<T> where T: IComparable<T>

    public static IEnumerable<IEnumerable<T>> Find(IEnumerable<T> numbers)
    
        return new IncreasingSubsetFinder<T>().find(numbers.GetEnumerator());
    

    IEnumerable<IEnumerable<T>> find(IEnumerator<T> iter)
    
        if (!iter.MoveNext())
            yield break;

        while (!done)
            yield return increasingSubset(iter);
    

    IEnumerable<T> increasingSubset(IEnumerator<T> iter)
    
        while (!done)
        
            T prev = iter.Current; 
            yield return prev;

            if ((done = !iter.MoveNext()) || iter.Current.CompareTo(prev) <= 0)
                yield break;
        
    

    bool done;

你会这样称呼:

List<int> data = new List<int>  1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;

foreach (var subset in IncreasingSubsetFinder<int>.Find(data))
    Console.WriteLine(string.Join(", ", subset));

【讨论】:

我不喜欢这依赖于List&lt;T&gt;;如果结果是从IEnumerable&lt;T&gt; 流式传输的,那么这将不起作用。 @casperOne 当然不会——但是 OP 有一个列表,所以这没问题。如果它需要在IEnumerable&lt;int&gt; 上运行,那么您必须以不同的方式进行。但是,此代码假定 YAGNI +1 您的回答完全符合规定的要求。恕我直言,在 OP 案例中使用 List&lt;T&gt; 时的最佳解决方案。【参考方案2】:

这不是典型的 LINQ 操作,因此在这种情况下(当坚持使用 LINQ 时)我建议使用Aggregate 方法:

var result = data.Aggregate(new List<List<int>>(), (r, n) =>

    if (r.Count == 0 || n <= r.Last().Last()) r.Add(new List<int>());
    r.Last().Add(n);
    return r;
);

【讨论】:

@sam,那听起来你不明白Aggregate 方法。 @Ivan,这与我汇总的答案相似。 Linq 操作通常不会记住他们已经查看过的项目,因此使用累加器是一个好主意。 @sam 无需冒犯,请保持好语气。你表达了你的意见——很好。不同的人,不同的口味。与 XAML 相比,有些人觉得 RegEx 更漂亮,而有些人则相反。我的代码 sn-p 怎么样,对我来说 Aggregate 相当于一个 foreach 循环,所以我以一种专注于主体代码块的方式对其进行格式化,这是解决方案的重要部分。我同意变量名称不是描述性的,但是如果您查看linq 标签的答案,您会发现使用短名称是一种常见的做法,例如xyz 等. 语气不错。对我来说,问题不是变量的聚合或短名称。您可以将 if 的内容放在新行上并避免使用 .Last().Last() 和 Last().Add()。正如我乍看之下所说,即使我完全理解它的作用,代码似乎也很混乱。因为它被 100 多位观众看到,所以我不想看到乱七八糟的东西,就像我在接待人之前整理我的家一样。祝你有美好的一天。【参考方案3】:

您可以使用索引来获取上一项并通过比较值来计算组 id。然后按组 ID 分组并取出值:

List<int> data = new List<int>  1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;

int groupId = 0;
var groups = data.Select
                 ( (item, index)
                   => new
                       Item = item
                      , Group = index > 0 && item <= data[index - 1] ? ++groupId : groupId
                      
                 );

List<List<int>> list = groups.GroupBy(g => g.Group)
                             .Select(x => x.Select(y => y.Item).ToList())
                             .ToList();

【讨论】:

+1,只是将计算“Group”的逻辑修改为index > 0 && item data[index - 1]? ++groupId : groupId【参考方案4】:

我真的很喜欢Matthew Watson's solution。但是,如果您不想依赖List&lt;T&gt;,这是我的简单通用方法,枚举可枚举最多一次,并且仍然保留惰性评估的能力。 p>

public static IEnumerable<IEnumerable<T>> AscendingSubsets<T>(this IEnumerable<T> superset) where T :IComparable<T>
     
    var supersetEnumerator = superset.GetEnumerator();

    if (!supersetEnumerator.MoveNext())
    
        yield break;
        

    T oldItem = supersetEnumerator.Current;
    List<T> subset = new List<T>()  oldItem ;

    while (supersetEnumerator.MoveNext())
    
        T currentItem = supersetEnumerator.Current;

        if (currentItem.CompareTo(oldItem) > 0)
        
            subset.Add(currentItem);
        
        else
        
            yield return subset;
            subset = new List<T>()  currentItem ;
        

        oldItem = supersetEnumerator.Current;
    

    yield return subset;

编辑:进一步简化了解决方案,只使用一个枚举器。

【讨论】:

【参考方案5】:

我已经修改了你的代码,现在可以正常工作了:

        List<int> data = new List<int>  1, 2, 1, 2, 3,3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;
        List<List<int>> resultLists = new List<List<int>>();
        int last = 0;
        int count = 0;

        var res = data.Where((p, i) =>
        
            if (i > 0)
            
                if (p > last && p!=last)
                
                    resultLists[count].Add(p);
                
                else
                
                    count++;
                    resultLists.Add(new List<int>());
                    resultLists[count].Add(p);
                
            
            else
            
                resultLists.Add(new List<int>());
                resultLists[count].Add(p);
            



            last = p;
            return true;
        ).ToList();

【讨论】:

【参考方案6】:

对于这样的事情,我通常不喜欢使用GroupBy 或其他实现结果的方法的解决方案。原因是您永远不知道输入序列将有多长,而实现这些子序列的成本可能非常高。

我更喜欢在拉取结果时流式传输结果。这允许流结果的IEnumerable&lt;T&gt; 的实现通过您对该流的转换继续流式传输。

请注意,如果您跳出对子序列的迭代并希望继续下一个序列,则此解决方案将不起作用;如果这是一个问题,那么实现子序列的解决方案之一可能会更好。

但是,对于整个序列的仅向前迭代(这是最典型的用例),这将工作得很好。

首先,让我们为我们的测试类设置一些帮助器:

private static IEnumerable<T> CreateEnumerable<T>(IEnumerable<T> enumerable)

    // Validate parameters.
    if (enumerable == null) throw new ArgumentNullException("enumerable");

    // Cycle through and yield.
    foreach (T t in enumerable)
        yield return t;


private static void EnumerateAndPrintResults<T>(IEnumerable<T> data,
    [CallerMemberName] string name = "") where T : IComparable<T>

    // Write the name.
    Debug.WriteLine("Case: " + name);

    // Cycle through the chunks.
    foreach (IEnumerable<T> chunk in data.
        ChunkWhenNextSequenceElementIsNotGreater())
    
        // Print opening brackets.
        Debug.Write(" ");

        // Is this the first iteration?
        bool firstIteration = true;

        // Print the items.
        foreach (T t in chunk)
        
            // If not the first iteration, write a comma.
            if (!firstIteration)
            
                // Write the comma.
                Debug.Write(", ");
            

            // Write the item.
            Debug.Write(t);

            // Flip the flag.
            firstIteration = false;
        

        // Write the closing bracket.
        Debug.WriteLine(" ");
    

CreateEnumerable 用于创建流式实现,EnumerateAndPrintResults 将获取序列,调用ChunkWhenNextSequenceElementIsNotGreater(即将出现并完成工作)并输出结果。

这里是实现。注意,我选择在IEnumerable&lt;T&gt; 上将它们实现为扩展方法;这是第一个好处,因为它不需要物化序列(从技术上讲,其他解决方案也没有,但最好像这样明确声明)。

一、入口点:

public static IEnumerable<IEnumerable<T>>
    ChunkWhenNextSequenceElementIsNotGreater<T>(
        this IEnumerable<T> source)
    where T : IComparable<T>

    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");

    // Call the overload.
    return source.
        ChunkWhenNextSequenceElementIsNotGreater(
            Comparer<T>.Default.Compare);


public static IEnumerable<IEnumerable<T>>
    ChunkWhenNextSequenceElementIsNotGreater<T>(
        this IEnumerable<T> source,
            Comparison<T> comparer)

    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (comparer == null) throw new ArgumentNullException("comparer");

    // Call the implementation.
    return source.
        ChunkWhenNextSequenceElementIsNotGreaterImplementation(
            comparer);

请注意,这适用于任何实现 IComparable&lt;T&gt; 或您提供 Comparison&lt;T&gt; 委托的地方;这允许您使用任何类型和任何类型的规则来执行比较。

下面是实现:

private static IEnumerable<IEnumerable<T>>
    ChunkWhenNextSequenceElementIsNotGreaterImplementation<T>(
        this IEnumerable<T> source, Comparison<T> comparer)

    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(comparer != null);

    // Get the enumerator.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    
        // Move to the first element.  If one can't, then get out.
        if (!enumerator.MoveNext()) yield break;

        // While true.
        while (true)
        
            // The new enumerator.
            var chunkEnumerator = new 
                ChunkWhenNextSequenceElementIsNotGreaterEnumerable<T>(
                    enumerator, comparer);

            // Yield.
            yield return chunkEnumerator;

            // If the last move next returned false, then get out.
            if (!chunkEnumerator.LastMoveNext) yield break;
        
    

注意:这使用另一个类ChunkWhenNextSequenceElementIsNotGreaterEnumerable&lt;T&gt; 来处理枚举子序列。此类将迭代从原始IEnumerable&lt;T&gt;.GetEnumerator() 调用获得的IEnumerator&lt;T&gt; 中的每个项目,但将最后一次调用的结果存储到IEnumerator&lt;T&gt;.MoveNext()

存储这个子序列生成器,并检查最后一次调用MoveNext 的值以查看是否命中了序列的末尾。如果有,那么它就简单地中断,否则,它会移动到下一个块。

这里是ChunkWhenNextSequenceElementIsNotGreaterEnumerable&lt;T&gt;的实现:

internal class 
    ChunkWhenNextSequenceElementIsNotGreaterEnumerable<T> : 
        IEnumerable<T>

    #region Constructor.

    internal ChunkWhenNextSequenceElementIsNotGreaterEnumerable(
        IEnumerator<T> enumerator, Comparison<T> comparer)
    
        // Validate parameters.
        if (enumerator == null) 
            throw new ArgumentNullException("enumerator");
        if (comparer == null) 
            throw new ArgumentNullException("comparer");

        // Assign values.
        _enumerator = enumerator;
        _comparer = comparer;
    

    #endregion

    #region Instance state.

    private readonly IEnumerator<T> _enumerator;

    private readonly Comparison<T> _comparer;

    internal bool LastMoveNext  get; private set; 

    #endregion

    #region IEnumerable implementation.

    public IEnumerator<T> GetEnumerator()
    
        // The assumption is that a call to MoveNext 
        // that returned true has already
        // occured.  Store as the previous value.
        T previous = _enumerator.Current;

        // Yield it.
        yield return previous;

        // While can move to the next item, and the previous 
        // item is less than or equal to the current item.
        while ((LastMoveNext = _enumerator.MoveNext()) && 
            _comparer(previous, _enumerator.Current) < 0)
        
            // Yield.
            yield return _enumerator.Current;

            // Store the previous.
            previous = _enumerator.Current;
        
    

    IEnumerator IEnumerable.GetEnumerator()
    
        return GetEnumerator();
    

    #endregion

这是问题中原始条件的测试以及输出:

[TestMethod]
public void Test***Condition()

    var data = new List<int>  
        1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 
    ;
    EnumerateAndPrintResults(data);

输出:

Case: Test***Condition
 1, 2 
 1, 2, 3 
 3 
 1, 2, 3, 4 
 1, 2, 3, 4, 5, 6 

这是相同的输入,但以可枚举的形式流式传输:

[TestMethod]
public void Test***ConditionEnumerable()

    var data = new List<int>  
        1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 
    ;
    EnumerateAndPrintResults(CreateEnumerable(data));

输出:

Case: Test***ConditionEnumerable
 1, 2 
 1, 2, 3 
 3 
 1, 2, 3, 4 
 1, 2, 3, 4, 5, 6 

这是一个非顺序元素的测试:

[TestMethod]
public void TestNonSequentialElements()

    var data = new List<int>  
        1, 3, 5, 7, 6, 8, 10, 2, 5, 8, 11, 11, 13 
    ;
    EnumerateAndPrintResults(data);

输出:

Case: TestNonSequentialElements
 1, 3, 5, 7 
 6, 8, 10 
 2, 5, 8, 11 
 11, 13 

最后,这是一个用字符代替数字的测试:

[TestMethod]
public void TestNonSequentialCharacters()

    var data = new List<char>  
        '1', '3', '5', '7', '6', '8', 'a', '2', '5', '8', 'b', 'c', 'a' 
    ;
    EnumerateAndPrintResults(data);

输出:

Case: TestNonSequentialCharacters
 1, 3, 5, 7 
 6, 8, a 
 2, 5, 8, b, c 
 a 

【讨论】:

这是最好的方法,但我认为您的代码可以通过在 yiling 之前保存子序列的最后一项并从那时起与该值进行比较来简化。你做的事情比这更复杂。你真的需要 ChunkWhenNextSequenceElementIsNotGreaterEnumerable 吗? @sam 您需要该类才能在子序列中保存对MoveNext 的最后一次调用的状态。如果你不存储它,那么你就无法确定序列是否已经结束并打破外循环。此外,您需要 something 来产生内部序列。它可能是一个函数,但您必须以某种方式将最后一次调用的结果返回给MoveNext。我想这可以通过Tuple&lt;T1, T2&gt; 来完成,但是将它分解成它自己的类(IMO)似乎更干净。【参考方案7】:

您可以使用 Linq 使用索引来计算组:

var result = data.Select((n, i) => new  N = n, G = (i > 0 && n > data[i - 1] ? data[i - 1] + 1 : n) - i )
                 .GroupBy(a => a.G)
                 .Select(g => g.Select(n => n.N).ToArray())
                 .ToArray();

【讨论】:

您的解决方案做了一些假设在 OP 的问题中不存在。您假设每个数字都增加一。 OP只提到较低的数字。您的解决方案适用于 OP 的示例,但不适用于此列表 1,3,1 根据描述应该生成两个列表,而不是三个。 @user2023861:是的,我要添加假设。 @user2023861:更新为 OP 的问题。【参考方案8】:

这是我使用一些产量的简单循环方法:

static IEnumerable<IList<int>> Split(IList<int> data) if (data.Count == 0) yield break; List<int> curr = new List<int>(); curr.Add(data[0]); int last = data[0]; for (int i = 1; i < data.Count; i++) if (data[i] <= last) yield return curr; curr = new List<int>(); curr.Add(data[i]); last = data[i]; yield return curr;

【讨论】:

【参考方案9】:

我使用字典来获取 5 个不同的列表,如下所示;

static void Main(string[] args)
    
        List<int> data = new List<int>  1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 ;
        Dictionary<int, List<int>> listDict = new Dictionary<int, List<int>>();

        int listCnt = 1;
        //as initial value get first value from list
        listDict.Add(listCnt, new List<int>());
        listDict[listCnt].Add(data[0]);


        for (int i = 1; i < data.Count; i++)
        
             if (data[i] > listDict[listCnt].Last())
            
                listDict[listCnt].Add(data[i]);
            
             else
            
                //increase list count and add a new list to dictionary
                listCnt++;
                listDict.Add(listCnt, new List<int>());
                listDict[listCnt].Add(data[i]);
            
        

        //to use new lists
        foreach (var dic in listDict)
        
            Console.WriteLine( $"List dic.Key : " + string.Join(",", dic.Value.Select(x => x.ToString()).ToArray()));

        

    

输出:

List 1 : 1,2
List 2 : 1,2,3
List 3 : 3
List 4 : 1,2,3,4
List 5 : 1,2,3,4,5,6

【讨论】:

以上是关于以递增的顺序将列表拆分为多个列表的主要内容,如果未能解决你的问题,请参考以下文章

使用 itertools 将列表拆分为递增的序列

列表访问汇总(下)

以递增的顺序迭代数对

PHP:将数据数组拆分为字母顺序

将列表拆分为两个列表的所有可能性

根据元组的值,以不同的顺序对元组列表进行排序。