【问题标题】:Split a List<int> into groups of consecutive numbers [closed]将 List<int> 拆分为连续数字组[关闭]
【发布时间】:2019-12-03 04:08:57
【问题描述】:

我有一个排序的List&lt;int&gt;,比如{ 1, 2, 3, 4, 6, 7, 9 }
我想把它分成几组——每组都有这样的连续数字:{ {1, 2, 3, 4}, {6, 7}, {9} }

我知道我可以使用for循环遍历列表,比较当前值和前一个值,然后决定是追加到最后一个组还是创建一个新组。但我想找到一种“漂亮”的方式来做到这一点。也许使用 LINQ?

编辑:

我从项目more-itertools中找到了一个python代码:

def consecutive_groups(iterable, ordering=lambda x: x):
    for k, g in groupby(
        enumerate(iterable), key=lambda x: x[0] - ordering(x[1])
    ):
        yield map(itemgetter(1), g)

【问题讨论】:

  • 你所说的“到某个切片”是什么意思?目前尚不清楚如何从 1 个集合到 3 个具有不同数量元素的集合。
  • @itsme86 我有同样的问题,直到我读到“连续数字”
  • 为什么for 循环不“漂亮”?我的猜测是 Linq 解决方案实际上会“更丑”恕我直言。
  • 编程的目标是写漂亮的代码还是高质量的代码?如果您遇到需要循环的问题,没有什么比循环更好的了。
  • 我很难理解为什么这个问题因为“基于意见”而获得如此多的密切投票。我认为似乎相当清楚的是,大多数人似乎都在考虑此类问题的“漂亮”一词是由于不熟悉英语而被使用的,而不是按字面意思理解的。

标签: c# linq


【解决方案1】:

这是取自http://bugsquash.blogspot.com/2010/01/grouping-consecutive-integers-in-c.html的扩展方法

public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> list) {
    var group = new List<int>();
    foreach (var i in list) {
        if (group.Count == 0 || i - group[group.Count - 1] <= 1)
            group.Add(i);
        else {
            yield return group;
            group = new List<int> {i};
        }
    }
    yield return group;
}

你可以这样使用它:

var numbers = new[] { 1, 2, 3, 4, 6, 7, 9 };
var groups = numbers.GroupConsecutive();

一旦 C# 7 发布,使用Span 可以更有效地避免创建新列表。


此更新版本无需分配任何列表即可完成此操作。

public static class EnumerableExtensions
{
    public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> list)
    {
        if (list.Any())
        {
            var count = 1;
            var startNumber = list.First();
            int last = startNumber;

            foreach (var i in list.Skip(1))
            {
                if (i < last)
                {
                    throw new ArgumentException($"List is not sorted.", nameof(list));
                }
                if (i - last == 1)
                    count += 1;
                else
                {
                    yield return Enumerable.Range(startNumber, count);
                    startNumber = i;
                    count = 1;
                }
                last = i;
            }
            yield return Enumerable.Range(startNumber, count);
        }
    }
}

【讨论】:

  • 但是@BradleyUffner,这有一个循环,它不漂亮! /s
  • 计数并返回Enumerable.Range而不是列表不是更容易吗?
  • 不错的解决方案。挑战:你可以在不分配任何列表的情况下不使用使用 Span 吗?
  • 未修复。首先:你需要在那里进行一次产量突破。第二:如果序列不是集合,Count() 效率低下。您无需数一数罐子中的所有便士来判断是否超过零。使用Any 检查序列中是否至少有一个元素。第三:为什么一个空列表的结果会产生一个包含一个空序列的序列?我原以为结果会是一个包含零序列的序列。
  • @BradleyUffner:我实际上还没有,你知道,运行代码,但是现在看起来不错。
【解决方案2】:

这是我对使用迭代器的扩展方法的建议:

public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> src) {
    var more = false; // compiler can't figure out more is assigned before use
    IEnumerable<int> ConsecutiveSequence(IEnumerator<int> csi) {
        int prevCurrent;
        do
            yield return (prevCurrent = csi.Current);
        while ((more = csi.MoveNext()) && csi.Current-prevCurrent == 1);
    }

    var si = src.GetEnumerator();
    if (si.MoveNext()) {
        do
            // have to process to compute outside level  
            yield return ConsecutiveSequence(si).ToList();
        while (more);
    }
}

不得不说 Python 算法很厉害,下面是它的 C# 实现:

public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> iterable, Func<int,int> ordering = null) {
    ordering = ordering ?? (n => n);
    foreach (var tg in iterable
                         .Select((e, i) => (e, i))
                         .GroupBy(t => t.i - ordering(t.e)))
        yield return tg.Select(t => t.e);
}

这里是 Python 算法的 C# 单行实现:

public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> iterable, Func<int,int> ordering = null) => 
    iterable
      .Select((e, i) => (e, i))
      .GroupBy(
        t => t.i - (ordering ?? (n => n))(t.e),
        (k,tg) => tg.Select(t => t.e));

注意:启用可空注释上下文的 C# 8 应在两种 Python 方法中使用 Func&lt;int,int&gt;?。您也可以使用??= 分配ordering

【讨论】:

  • C# 迭代器方法陷入延迟执行陷阱。试试var result = input.GroupConsecutive().ToList(),你会看到的。虽然我相信使用GetEnumerator 的实现是正确的方法,但当前的实现是错误的。过于注重简洁而不是功能。毕竟,拥有一个自定义扩展方法很重要,以允许简洁的使用,而不是简洁的实现。
  • @IvanStoev 有趣的是 LINQPad Dump()ToList() 不工作的地方工作......我不确定它是否可以在不需要额外存储的情况下修复:(
  • 我将答案更改为使用ToList 来保存子序列:(
【解决方案3】:

@Bradley Uffner 和@NetMage 非分配迭代器方法的正确实现是这样的:

public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> source)
{
    using (var e = source.GetEnumerator())
    {
        for (bool more = e.MoveNext(); more; )
        {
            int first = e.Current, last = first, next;
            while ((more = e.MoveNext()) && (next = e.Current) > last && next - last == 1)
                last = next;
            yield return Enumerable.Range(first, last - first + 1);
        }
    }
}

即使对于无序输入,它也能正常工作,仅迭代源序列一次,并正确处理所有极端情况和整数上溢/下溢。唯一失败的情况是连续范围计数大于int.MaxValue

但是看看你的follow up question,可能下面的实现会更好地满足你的需求:

public static IEnumerable<(int First, int Last)> ConsecutiveRanges(this IEnumerable<int> source)
{
    using (var e = source.GetEnumerator())
    {
        for (bool more = e.MoveNext(); more;)
        {
            int first = e.Current, last = first, next;
            while ((more = e.MoveNext()) && (next = e.Current) > last && next - last == 1)
                last = next;
            yield return (first, last);
        }
    }
}

【讨论】:

  • 也许你自己版本的RangeIterator 使用long 来计数可以解决这个问题?类似static IEnumerable&lt;int&gt; RangeIterator(int start, long count) { count += start; for (int i = start; i &lt; count; i++) yield return i; }
  • @NetMage 确实!但我刚刚意识到该方法可以简单地产生范围,然后(如果需要)可以很容易地转换为具有简单 Select 和标准/建议自定义 RangeIterator 的序列。
【解决方案4】:

试试下面的代码;

public static IEnumerable<IEnumerable<int>> GroupConsecutive(this IEnumerable<int> source)
{
    if (!source.Any()) { yield break;}
    var prev = source.First();
    var grouped = new List<int>(){ prev };
    source = source.Skip(1);
    while (source.Any())
    {
        var current = source.First();
        if (current - prev != 1)
        {
            yield return grouped;
            grouped = new List<int>();
        }
        grouped.Add(current);
        source = source.Skip(1);
        prev = current;
    }
    yield return grouped;
}

var numbers = new[] { 1, 2, 3, 4, 6, 7, 9 };
var result = numbers.GroupConsecutive();

Output
1,2,3,4
6,7
9

【讨论】:

  • 如果source 中没有项目,它会抛出异常(我的代码中最初也有同样的问题)。
  • @Bradley Uffner,谢谢提醒。
猜你喜欢
  • 1970-01-01
  • 2022-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-22
  • 1970-01-01
相关资源
最近更新 更多