繁体   English   中英

如何使用Linq从List <>计算值的范围?

[英]How to calculate range of values from List<> with Linq?

我在C#中有一个有序数字列表,我想用LINQ计算可以根据其安全值取的最小值和最大值

该列表始​​终是有序的,永远不会为空。

例如:

我的清单物件:

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

我的预期结果:

1060-1090
6368-6384
30165-30215

谢谢。

对于此类问题, 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

考虑为IEnumerable<TSource>创建扩展方法,因此可以像使用LINQ函数一样使用它。 参见神秘的扩展方法

您的示例没有处理几个问题:

  • 如果您的输入序列为空怎么办?
  • 如果未订购输入怎么办?
  • 如果您多次获得相同的值怎么办: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<int, int>的序列返回。 如果需要,您可以为此创建一个专用的类。

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);
//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]);

如果实现简单的配对类,则可以使用.Aggregate() LINQ方法。 由于Tuple是不可变的,因此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;
    });

使用我的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()

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM