Используя LINQ, чтобы найти накопительную сумму массива чисел в C#
У меня есть строка csv, содержащая double (например, "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();
который дает числа в виде массива, но не совокупную сумму чисел.
Есть ли способ продолжить это выражение, чтобы получить то, что я хочу, или мне нужно использовать итерацию для создания нового массива из массива, который у меня уже есть?
9 ответов
var input=new double[]{ ... }
double sum=0;
var output=input
.Select(w=>sum+=w);
Есть время для обобщения, и есть время для решения поставленной задачи. Это один из последних раз. Если вы хотите создать метод, который превращает последовательность двойников в последовательность частичных сумм, просто сделайте это:
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();
и теперь вы не получите исключение, если пользователь допустит опечатку; Вы получаете нули.
У меня было подобное требование некоторое время назад. По сути, мне нужно было выполнить агрегацию, но мне также нужно было выбрать каждое промежуточное значение. Поэтому я написал метод расширения с именем 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;
}
}
Вы хотите использовать Aggregate
operator, with a List<double>
as the aggregation accumulator. That way you can produce a projection which is itself a sequence of sums.
Вот пример для начала:
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();
Почему это должно быть 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]);
Прежде всего, я не думаю, что это хорошая задача для 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
);
Используйте RX:
var input=new double[]{ ... }
var output = new List<double>();
input.ToObservable().Scan((e, f) => f + e).Subscribe(output.Add);
Вот мое решение:
- Linq
- линейное время
- линейная память
- без побочных эффектов
Единственное предостережение в том, что это не работает для пустых списков (тривиально для обработки).
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)
}
);
Это довольно просто обобщить с помощью генератора. Вот новый метод расширения под названием Accumulate
это работает как комбинация Select
а также Aggregate
, Он возвращает новую последовательность, применяя двоичную функцию к каждому элементу в последовательности и накопленном значении.
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);
}
}
Вот способ сделать это с помощью 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
Накопительная сумма за List<double>
:
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; });