简体   繁体   English

在 Linq 中求累计和的最小值和最大值

[英]Find min and max of cumulative sum in Linq

I have the following function which I am using to find the terminal accumulative positive and negative value, which is working:我有以下 function,我用它来查找终端累计正值和负值,它正在工作:

public class CumulativeTotal
{
    [Test]
    public void CalculatesTerminalValue()
    {
        IEnumerable<decimal> sequence = new decimal[] { 10, 20, 20, -20, -50, 10 };

        var values = FindTerminalValues(sequence);
        Assert.That(values.Item1, Is.EqualTo(-20));
        Assert.That(values.Item2, Is.EqualTo(50));

        Assert.Pass();
    }

    public static Tuple<decimal,decimal> FindTerminalValues(IEnumerable<decimal> values)
    {
        decimal largest = 0;
        decimal smallest = 0;
        decimal current = 0;

        foreach (var value in values)
        {
            current += value;
            if (current > largest)
                largest = current;
            else if (current < smallest)
                smallest = current;
        }

        return new Tuple<decimal, decimal>(smallest,largest);
    }
}

However, in the interests of learning, how could i implement with Linq?但是,为了学习,我怎么能用Linq来实现呢?

I can see a package MoreLinq , but not sure where to start!我可以看到 package MoreLinq ,但不确定从哪里开始!

yes, you can use MoreLinq like this, it has the Scan method.是的,你可以像这样使用MoreLinq ,它有Scan方法。

public static Tuple<decimal, decimal> FindTerminalValues(IEnumerable<decimal> values)
{
    var cumulativeSum = values.Scan((acc, x) => acc + x).ToList();

    decimal min = cumulativeSum.Min();
    decimal max = cumulativeSum.Max();

    return new Tuple<decimal, decimal>(min, max);
}

The Scan extension method generates a new sequence by applying a function to each element in the input sequence, using the previous element as an accumulator. Scan 扩展方法通过将 function 应用于输入序列中的每个元素并使用前一个元素作为累加器来生成一个新序列。 In this case, the function is simply the addition operator, so the Scan method generates a sequence of the cumulative sum of the input sequence.在这种情况下,function 只是加法运算符,因此 Scan 方法生成一个输入序列的累加和序列。

You can try standard Linq Aggregate method:您可以尝试标准的 Linq Aggregate方法:

// Let's return named tuple: unlike min, max 
// current .Item1 and .Item2 are not readable
public static (decimal min, decimal max) FindTerminalValues(IEnumerable<decimal> values) {
  //public method arguments validation
  if (values is null)
    throw new ArgeumentNullValue(nameof(values));

  (var min, var max, _) = values
    .Aggregate((min : 0m, max : 0m, curr : 0m), (s, a) => (
       Math.Min(s.min, s.curr + a), 
       Math.Max(s.max, s.curr + a),
       s.curr + a));

  return (min, max);
}

The major flaw in the code you've presented is that if the running sum of the the sequence stays below zero or above zero the whole time then the algorithm incorrectly returns zero as one of the terminals.您提供的代码的主要缺陷是,如果序列的运行总和始终保持在零以下或零以上,则算法错误地返回零作为终端之一。

Take this:拿着它:

IEnumerable<decimal> sequence = new decimal[] { 10, 20, };

Your current algorithm returns (0, 30) when it should be (10, 30) .您当前的算法在应该返回(0, 30) (10, 30)

To correct that you must start with the first value of the sequence as the default minimum and maximum.要纠正这一点,您必须从序列的第一个值开始作为默认的最小值和最大值。

Here's an implementation that does that:这是一个执行此操作的实现:

public static (decimal min, decimal max) FindTerminalValues(IEnumerable<decimal> values)
{
    if (!values.Any())
        throw new System.ArgumentException("no values");
        
    decimal first = values.First();
    
    IEnumerable<decimal> scan = values.Scan((x, y) => x + y);

    return scan.Aggregate(
        (min: first, max: first),
        (a, x) =>
        (
            min: x < a.min ? x : a.min, 
            max: x > a.max ? x : a.max)
        );
}

It uses System.Interactive to get the Scan operator (but you could use MoreLinq .它使用System.Interactive来获取Scan运算符(但您可以使用MoreLinq

However, the one downside to this approach is that IEnumerable<decimal> is not guaranteed to return the same values every time.但是,这种方法的一个缺点是不能保证IEnumerable<decimal>每次都返回相同的值。 You either need to (1) pass in a decimal[] , List<decimal> , or other structure that will always return the same sequence, or (2) ensure you only iterate the IEnumerable<decimal> once.您需要 (1) 传入decimal[]List<decimal>或其他始终返回相同序列的结构,或者 (2) 确保您只迭代IEnumerable<decimal>一次。

Here's how to do (2):方法 (2) 如下:

public static (decimal min, decimal max) FindTerminalValues(IEnumerable<decimal> values)
{
    var e = values.GetEnumerator();
    if (!e.MoveNext())
        throw new System.ArgumentException("no values");

    var terminal = (min: e.Current, max: e.Current);
    decimal value = e.Current;

    while (e.MoveNext())
    {
        value += e.Current;
        terminal = (Math.Min(value, terminal.min), Math.Max(value, terminal.max));
    }

    return terminal;
}

You can use the Aggregate method in LINQ to achieve this.可以使用LINQ中的Aggregate方法来实现。 The Aggregate method applies a function to each element in a sequence and returns the accumulated result. Aggregate 方法将 function 应用于序列中的每个元素并返回累积结果。 It takes as parameter an initial accumulator object to keep track of the smallest and largest function.它以初始累加器 object 作为参数来跟踪最小和最大的 function。

public static Tuple<decimal,decimal> FindTerminalValues(IEnumerable<decimal> values)
{
    return values.Aggregate(
        // Initial accumulator value:
        new Tuple<decimal, decimal>(0, 0),
        // Accumulation function:
        (acc, value) =>
        {
            // Add the current value to the accumulator:
            var current = acc.Item1 + value;
            // Update the smallest and largest accumulated values:
            var smallest = Math.Min(current, acc.Item1);
            var largest = Math.Max(current, acc.Item2);
            // Return the updated accumulator value:
            return new Tuple<decimal, decimal>(smallest, largest);
        });
}

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

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