【问题标题】:How to calculate range of values from List<> with Linq?如何使用 Linq 从 List<> 计算值的范围?
【发布时间】:2019-04-28 18:47:33
【问题描述】:

我有一个 C# 中的有序数字列表,我想使用 LINQ 计算可以根据它们的 secuencial 值取的最小值和最大值

列表总是有序的,永远不会是空的。

例如:

我的列表对象:

1060
1061
....
1089
1090
6368
6369
....
6383
6384
30165
30166
....
30214
30215

我的预期结果:

1060-1090
6368-6384
30165-30215

谢谢。

【问题讨论】:

  • 为什么选择 LINQ?你将如何处理像5, 7, 9 这样的序列?
  • 列表总是有序的吗?
  • 是的,我编辑了我的帖子。

标签: c# linq


【解决方案1】:
//Sample list of ordered integers
List<int> lst = new List<int>{101,102,103,104,106,107,108,111,112,114,120,121};

// find minimum element of each sub-sequence within the above list
var minBoundaries = lst.Where(i => !lst.Contains(i-1)).ToList();

// find maximum element of each sub-sequence within the above list
var maxBoundaries = lst.Where(i => !lst.Contains(i+1)).ToList();

//format minimum and maximum elements of each sub-sequence as per the sample output in the question
var result = new List<string>();
for(int i = 0; i < maxBoundaries.Count; i++) 
    result.Add(minBoundaries[i]+"-"+maxBoundaries[i]);

【讨论】:

  • 谢谢。这是最简单的解决方案。
【解决方案2】:

对于此类问题,Zip 方法很方便。这就是它的作用:

对两个序列的对应元素应用一个指定的函数,产生一个结果序列。

它可以用来配对一个序列的连续元素,通过压缩序列本身。

var source = new List<int> { 1, 2, 3, 4, 5, 11, 12, 13, 21, 22 };
var gaps = source
    .Zip(source.Skip(1), (n1, n2) => (n1, n2, gap: n2 - n1)) // Calculate the gaps
    .Where(e => e.gap != 1) // Select non sequential pairs
    .ToArray();
var gapsEx = gaps
    .Prepend((n1: 0, n2: source.First(), gap: 0)) // Add the first element
    .Append((n1: source.Last(), n2: 0, gap: 0)) // Add the last element
    .ToArray();
var results = gapsEx
    .Zip(gapsEx.Skip(1), (e1, e2) => (from: e1.n2, to: e2.n1)); // Pairwise gaps

Console.WriteLine($"Results: {String.Join(", ", results.Select(r => r.from + "-" + r.to))}");

输出:

结果:1​​-5、11-13、21-22

【讨论】:

    【解决方案3】:

    考虑为IEnumerable&lt;TSource&gt; 创建一个扩展方法,这样您就可以像使用 LINQ 函数一样使用它。见Extension Methods Demystified

    你的例子没有处理几个问题:

    • 如果您的输入序列为空怎么办?
    • 如果输入没有排序怎么办?
    • 如果你有好几次相同的值:1 2 3 3 3 3 4 5?
    • 如果您的子序列只有一个连续数字:1 2 7 18 19?

    所以让我们给出一个适当的要求:

    给定一个整数输入序列,创建一个整数对输出序列,其中值是输入序列中连续数字序列的第一个和最后一个数字。

    例子:

    • 1060 1061 ... 1089 1090 6368 6369 ... 6384 30165 ... => [1060, 1090] [6369, 6384] [30165
    • 2 3 4 5 17 18 19 4 5 6 7 1 2 3 4 5 => [2, 5] [17, 19] [4, 7] [1 5]
    • 2 3 4 5 6 8 9 => [2, 5] [6, 6] [8, 9]

    我会将对的序列作为Tuple&lt;int, int&gt; 的序列返回。如果需要,您可以为此创建一个专用类。

    static IEnumerable<Tuple<int, int>> ToMinMaxTuples(this IEnumerable<int> source)
    {
        // TODO: source == null
        var enumerator = source.GetEnumerator();
        if (enumerator.MoveNext())
        {
            // there is at least one item in source
            int min = enumerator.Current;
            int max = min;
            while (enumerator.MoveNext())
            {
                // there is another item in the sequence
                if (enumerator.Current == max + 1)
                {
                    // current is part of the current sequence, continue with next number
                    max = enumerator.Current;
                }
                else
                {
                    // current is not part of the current sequence,
                    // it is the start of the next one
                    // yield return [min, max] as a Tuple:
                    yield return new Tuple<int, int>(min, max);
    
                    // start the next sequence:
                    min = enumerator.Current;
                    max = min;
                }
            }
        }
    }
    

    用法:

    IEnumerable<Tuple<int, int>> result = myInputList.ToMinMaxTuples();
    

    或者在一些大的 LINQ 语句的中间:

    var result = Students
        .Where(student => student.Country == "Republique Française")
        .Select(student => student.Grade)
        .ToMinMaxTuples()
        .OrderBy(tuple => tuple.Item1)
        .ThenBy(tuple => tuple.Item2);
    

    【讨论】:

    • 我认为您的实施存在错误。最后一个范围没有产生。顺便说一句,不要忘记dispose your enumerators
    • 你是对的:最后一个范围没有产生。如果我有时间(并且如果我记得这样做),我会纠正它。也许其他人愿意加上最后一对的收益率回报。当然,我们应该将 GetEnumerator 放在 using 语句中
    • 谢谢哈拉尔德。我会试试你的解决方案。
    【解决方案4】:

    如果你实现了一个简单的pair类,那么你可以使用.Aggregate()LINQ方法。 由于元组是不可变的,因此 pair 类是必要的,但它可以很容易地像这样构造......

    public class MinMaxPair<T>
    {
        public MinMaxPair(T min, T max)
        {
            Min = min;
            Max = max;
        }
    
        public T Min;
        public T Max;
    }
    

    有了这个,.Aggregate() 调用就变成了

    nums.Aggregate(
        new List<MinMaxPair<int>>(),
        (sets, next) =>
        {
            if (!sets.Any() || next - sets.Last().Max > 1)
            {
                sets.Add(new MinMaxPair<int>(next, next));
            }
            else
            {
                var minMax = sets.Last();
                if (next < minMax.Min)
                    minMax.Min = next;
                else
                    minMax.Max = next;
            }
            return sets;
        });
    

    【讨论】:

      【解决方案5】:

      使用我的Scan扩展方法的一对增强版,它基于类似于聚合的APL扫描运算符,但返回中间结果,我创建了变量广义分组方法。使用GroupByPairsWhile,我(以前)为这类问题创建了一个GroupBySequential 方法。

      public static class IEnumerableExt {
          // TKey combineFn((TKey Key, T Value) PrevKeyItem, T curItem):
          // PrevKeyItem.Key = Previous Key
          // PrevKeyItem.Value = Previous Item
          // curItem = Current Item
          // returns new Key
          public static IEnumerable<(TKey Key, T Value)> ScanToPairs<T, TKey>(this IEnumerable<T> src, TKey seedKey, Func<(TKey Key, T Value), T, TKey> combineFn) {
              using (var srce = src.GetEnumerator())
                  if (srce.MoveNext()) {
                      var prevkv = (seedKey, srce.Current);
      
                      while (srce.MoveNext()) {
                          yield return prevkv;
                          prevkv = (combineFn(prevkv, srce.Current), srce.Current);
                      }
                      yield return prevkv;
                  }
          }
      
          // bool testFn(T prevItem, T curItem)
          // returns groups by runs of matching bool
          public static IEnumerable<IGrouping<int, T>> GroupByPairsWhile<T>(this IEnumerable<T> src, Func<T, T, bool> testFn) =>
              src.ScanToPairs(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1)
                 .GroupBy(kvp => kvp.Key, kvp => kvp.Value);
      
          public static IEnumerable<IGrouping<int, int>> GroupBySequential(this IEnumerable<int> src) => src.GroupByPairsWhile((prev, cur) => prev + 1 == cur);
      
      }
      

      使用扩展方法,你的问题很简单:

      var ans = src.GroupBySequential().Select(g => new { Min = g.Min(), Max = g.Max() });
      

      这假定列表没有排序。如果已知列表是有序的,您可以使用First()Last() 而不是Min()Max()

      注意:扩展方法可能看起来很复杂,但它们为多种不同类型的分组提供了基础,包括按相等项目的运行分组、按通用测试函数分组,以及处理第一个分组的各种种子和结束策略和成对工作时的最后一个元素。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多