使用 LINQ 在 C# 中查找数字数组的累积和

Posted

技术标签:

【中文标题】使用 LINQ 在 C# 中查找数字数组的累积和【英文标题】:using LINQ to find the cumulative sum of an array of numbers in C# 【发布时间】:2011-06-16 22:41:30 【问题描述】:

我有一个包含双精度数的 csv 字符串(例如“0.3,0.4,0.3”),我希望能够输出一个包含这些数字的累积和的双精度数组(例如 [0.3,0.7,1.0])。

到目前为止,我有

double[] probabilities = textBox_f.Text.Split(new char[]',').Select(s => double.Parse(s)).ToArray();

它以数组的形式给出数字,但不是数字的累积和。

有没有办法继续这个表达式来得到我想要的,或者我需要使用迭代从我已经拥有的数组中创建一个新数组?

【问题讨论】:

我喜欢学习新技术和做事方式。完全有可能其他方法更好或更快,但这是我不知道该怎么做,所以想 为什么?如果没有 LINQ 的解决方案可以更快地键入,那么您为什么要对 LINQ 解决方案感兴趣?以及为什么特别是 LINQ ——为什么不询问使用泛型或dynamic 或任何其他不需要回答问题的随机特性的解决方案? Split(new char[]',') 可以等效地写成Split(','),因为参数是用params 声明的。 老问题,但我必须发表评论。问题是你如何在 LINQ 中做到这一点,而不是你如何在没有 LINQ 的情况下做到这一点。 @simonalexander2005 我为你提出的问题鼓掌,但我想知道为什么你选择了 Blindy 的答案(目前)有 -3 分?这绝对不是一个好的答案。我个人更喜欢 Eric Lippert 或 Andrey 的答案。只是想知道您在将近 5 年后对这个问题的看法。 【参考方案1】:

总有一段时间,解决实际提出的问题也有一段时间。这是后者之一。如果您想创建一个将双精度序列转换为部分和序列的方法,那么只需这样做:

public static IEnumerable<double> CumulativeSum(this IEnumerable<double> sequence)

    double sum = 0;
    foreach(var item in sequence)
    
        sum += item;
        yield return sum;
            

简单。不要乱用聚合和复杂的查询等等。易于理解、易于调试、易于使用:

textBox_f.Text
    .Split(new char[]',')
    .Select(s => double.Parse(s))
    .CumulativeSum()
    .ToArray();

现在,我注意到如果那是用户输入,那么 double.Parse 会抛出异常;这样做可能是一个更好的主意:

public static double? MyParseDouble(this string s)

    double d;
    if (double.TryParse(s, out d))
        return d;
    return null;


public static IEnumerable<double?> CumulativeSum(this IEnumerable<double?> sequence)

    double? sum = 0;
    foreach(var item in sequence)
    
        sum += item;
        yield return sum;
            

...
textBox_f.Text
    .Split(new char[]',')
    .Select(s => s.MyParseDouble())
    .CumulativeSum()
    .ToArray();

现在,如果用户输入错误,您不会收到异常;你得到空值。

【讨论】:

我尝试使用以下代码double? sum = 0; sum += 1; sum += null; Console.WriteLine($"sum = sum."); 模拟double? 数组的部分和。 sum 变为 null 而不是 1。怎么了? @MoneyOrientedProgrammer:没有错;这是正确的。 null double 表示“我不知道值是什么”。什么是零加一加你不知道的东西? @MoneyOrientedProgrammer:如果您的意图是“空值为零”,则使用x??0,其中x 可以为空。 好的。你上面的代码不需要写成sum += item??0 ? @MoneyOrientedProgrammer:这取决于业务逻辑。就个人而言,我不希望在没有警告的情况下忽略格式错误的数据。【参考方案2】:

前段时间我也有类似的要求。基本上,我需要进行聚合,但我还需要选择每个中间值。所以我写了一个名为SelectAggregate的扩展方法(可能不是最合适的名字,但我找不到更好的名字)可以这样使用:

double[] numbers = new []  0.3, 0.4, 0.3 ;
double[] cumulativeSums = numbers.SelectAggregate(0.0, (acc, x) => acc + x).ToArray();

代码如下:

    public static IEnumerable<TAccumulate> SelectAggregate<TSource, TAccumulate>(
        this IEnumerable<TSource> source,
        TAccumulate seed,
        Func<TAccumulate, TSource, TAccumulate> func)
    
        source.CheckArgumentNull("source");
        func.CheckArgumentNull("func");
        return source.SelectAggregateIterator(seed, func);
    

    private static IEnumerable<TAccumulate> SelectAggregateIterator<TSource, TAccumulate>(
        this IEnumerable<TSource> source,
        TAccumulate seed,
        Func<TAccumulate, TSource, TAccumulate> func)
    
        TAccumulate previous = seed;
        foreach (var item in source)
        
            TAccumulate result = func(previous, item);
            previous = result;
            yield return result;
        
    

【讨论】:

+1 — 很好的解决方案,还教授如何编写可重用代码!【参考方案3】:

您想使用Aggregate 运算符,并将List&lt;double&gt; 作为聚合累加器。这样你就可以产生一个投影,它本身就是一个总和序列。

下面是一个帮助您入门的示例:

double[] runningTotal = textBox_f.Text
            .Split(new char[]',')
            .Select(s => double.Parse(s))
            .Aggregate((IEnumerable<double>)new List<double>(), 
                       (a,i) => a.Concat(new[]a.LastOrDefault() + i))
            .ToArray();

【讨论】:

这个效率低下,每次都会计算Sum O(n^2) 很好的答案,让我看到了聚合 - 但最后我选择了另一个使用。不过谢谢 @Andrey - 实际上,我最初的实现是不正确的。我的意思是使用 LastOrDefault() ...因为运行总计中的最后一项已经是它之前所有项目的累积总和。剩下的唯一不足是创建中间序列……对于大多数用例来说,这应该是最小的。实现现在编译并产生预期的结果。【参考方案4】:
var input=new double[] ... 
double sum=0;

var output=input
    .Select(w=>sum+=w);

【讨论】:

使用 LINQ 查询只是因为它们的副作用是一个坏主意,原因有很多——其中最重要的是代码的未来读者不会想到这一点......它违反了最小意外原则。 这是一个非常非常糟糕的主意。布希金是对的。 请不要编写有副作用的查询。查询应该查询数据,而不是修改数据。 @Blindy:您能解释一下 Select 与 map 有何不同吗? (以及作为查询理解“功能”的副作用的变量突变如何?) 作为一个具体的例子来说明为什么这是一个糟糕的主意,var l1 = output.ToList(); var l2 = output.ToList(); 导致列表具有不同内容的事实可能非常令人惊讶。 @Blindy:我希望 Haskell 程序员会对你所做的事情感到非常震惊。 Haskell 就是要避免这样的副作用和突变。【参考方案5】:

为什么需要 LINQ?

var cumulative = new double[probabilities.Length];
for (int i = 0; i < probabilities.Length; i++)
    cumulative[i] = probabilities[i] + (i == 0 ? 0 : cumulative[i-1]);

【讨论】:

也许出于同样的原因,它必须是累积和,一个数字数组和 C#?【参考方案6】:

首先,我不认为这对 Linq 来说是件好事。普通的旧foreach 会做得更好。但作为一个谜题,这很好。

第一个想法是使用子查询,但我不喜欢它,因为它是 O(n^2)。这是我的线性解决方案:

        double[] probabilities = new double[]  0.3, 0.4, 0.3;
        probabilities
            .Aggregate(
                new sum=Enumerable.Empty<double>(), last = 0.0d,
                (a, c) => new 
                    sum = a.sum.Concat(Enumerable.Repeat(a.last+c,1)),
                    last = a.last + c
                ,
                a => a.sum
            );

【讨论】:

我知道是七年后,但直到现在我才看到这个解决方案。虽然这个解决方案在时间上是线性的,但在堆栈空间的使用上也是线性的,这真的很糟糕;相对较小的输入数组会导致堆栈溢出。你看到为什么它在堆栈空间中是线性的吗?提示:溢出出现在您没有编写的代码中。编写该代码,看看会发生什么! @EricLippert 这是一个有趣的评论。我不确定什么会导致线性堆栈空间分配。 Concat 是否通过将序列存储在堆栈中来有效地产生序列? github.com/Microsoft/referencesource/blob/master/System.Core/… 这是一个小程序。在运行它之前,请尝试预测输出将是什么。那就试试吧;你感到惊讶吗? dotnetfiddle.net/9akiA5 现在应该清楚堆栈的消耗位置了。 @EricLippert 是的,看来我的猜测是正确的,ConcatIterator 使用堆栈来遍历列表。感谢您指出它,我不知道使用Concat 来处理长列表是不明智的。 只是为了澄清您的评论:问题是当您有大量连接时。将两个序列连接在一起很好。当您将一个项目连接到一个序列上,然后将一个项目连接到该序列上,然后将一个项目连接到该序列上,然后将一个项目连接到该序列上,就会出现问题……您会看到这是怎么回事。我们在堆上构建一个对象链,在迭代时每个都会触发一个方法调用,而调用进入堆栈。【参考方案7】:

使用 RX:

var input=new double[] ... 
var output = new List<double>();
input.ToObservable().Scan((e, f) => f + e).Subscribe(output.Add);

【讨论】:

虽然代码不错,但确实需要额外的导入【参考方案8】:

这实际上很容易用生成器进行泛化。这是一个名为Accumulate 的新扩展方法,它的工作方式类似于SelectAggregate 的组合。它通过对序列中的每个元素和到目前为止的累积值应用二进制函数来返回一个新序列。

 public static class EnumerableHelpers 
 
    public static IEnumerable<U> Accumulate<T, U>(this IEnumerable<T> self, U init, Func<U, T, U> f) 
    
        foreach (var x in self)
            yield return init = f(init, x);
    

    public static IEnumerable<T> Accumulate<T>(this IEnumerable<T> self, Func<T, T, T> f)
    
        return self.Accumulate(default(T), f);
    

    public static IEnumerable<double> PartialSums(this IEnumerable<double> self)
    
        return self.Accumulate((x, y) => x + y);
    

    public static IEnumerable<int> PartialSums(this IEnumerable<int> self)
    
        return self.Accumulate((x, y) => x + y);
    
 

【讨论】:

【参考方案9】:

这是使用 LINQ 的一种方法:

double[] doubles =  1.7, 2.3, 1.9, 4.1, 2.9 ;
var doublesSummed = new List<double>();

Enumerable.Aggregate(doubles, (runningSum, nextFactor) => 
    double currentSum = runningSum + nextFactor;
    doublesSummed.Add(currentSum);
    return currentSum;
);

doublesSummed.Dump();

在 LINQPad 中:

4 5.9 10 12.9

【讨论】:

【参考方案10】:

这是我的解决方案:

林克 线性时间 线性内存 无副作用

唯一需要注意的是它不适用于空列表(处理起来很简单)。

    var doublesSummed  = doubles.Skip(1).Aggregate(
        new 
            sum = doubles.First(),
            doubles = new [] doubles.First().AsEnumerable()
        ,  
        (acc, nextDouble) => new 
            sum = acc.sum + nextDouble,
            doubles = acc.doubles.Append(acc.sum + nextDouble)
        
    );

Demo

【讨论】:

【参考方案11】:

List&lt;double&gt;的累计总和:

var nums = new List<double>()  0.3, 0.0, 0.4, 1.1 ;
var cumsum = nums.Aggregate(new List<double> (), 
              (list, next) =>  list.Add(list.LastOrDefault() + next); return list; );

【讨论】:

以上是关于使用 LINQ 在 C# 中查找数字数组的累积和的主要内容,如果未能解决你的问题,请参考以下文章

如何在 LINQ 中计算累积和?

查找累积和反应打字稿

使用 Linq C# 在 DataTable 中查找重叠的日期

在c#中使用lambda或linq查找项目索引[关闭]

C# LINQ 在列表中查找重复项

如何在 C# 数组中查找和显示特定值