【问题标题】:LINQ Min/Max and minimizing iterationsLINQ 最小/最大和最小化迭代
【发布时间】:2017-11-27 22:08:02
【问题描述】:

我正在编写一个函数,它接受一系列 System.Windows.Point 并返回一个 ValueTuple,其中包含所有点的边界 X 和 Y 值。这是为了确定图形轴的标签。

我正在尝试将我执行的列表的迭代次数减少到一次。经过大量的谷歌搜索,我已经适应(阅读:“复制”)一种类似下面的方法,我被告知可以做到这一点。但我不确定如何验证这一事实。我想知道是否有人更熟悉 LINQ 可以

  1. 确认以下函数确实只会迭代列表一次,即使它正在计算 4 个不同的值
  2. 如果是这样,请向我解释这是怎么回事。因为在我看来,正在构造的匿名类型在给定列表上每次调用两次“Min”和“Max”。为什么这不会导致 4 次迭代?
  3. 也许甚至可以解释一下我是如何自己验证发生的迭代次数的,这样以后我就不需要问这样的问题了。我不知道该怎么做。

我的 LINQ-Fu 还不强。

谢谢

    /// <summary>
    /// X and Y axis boundaries in the form of a System.ValueTuple.
    /// </summary>
    public (double MinX, double MaxX, double MinY, double MaxY) 
    GetBounds(List<System.Windows.Point> pts)
    {


        // Calculate the bounds with a LINQ statement.  Is this one iteration or many?

        var a = pts.GroupBy(i => 1).Select(
            pp => new
            {
                MinY = pp.Min(p => p.Y),
                MaxY = pp.Max(p => p.Y),
                MinX = pp.Min(p => p.X),
                MaxX = pp.Max(p => p.X)
            }).FirstOrDefault();


        return a != null ? (a.MinX, a.MaxX, a.MinY, a.MaxY) : (0, 0, 0, 0);
    }

【问题讨论】:

  • 要验证它,您可以实现自己的IEnumerable&lt;Point&gt;,它会在迭代时写入控制台。
  • 您不使用输入参数是否有原因?你怎么知道这个集合被迭代了多次?
  • 首先,我很抱歉。 “ProfilePoints”应该是输入参数。我在最后一刻编辑了我的帖子。现在修好了。其次,我不知道它是否被多次迭代。这就是我想要确定的

标签: c# linq c#-7.0


【解决方案1】:

确认下面的函数确实只会迭代列表一次,即使它正在计算 4 个不同的值

否 - 原始列表将有效地迭代 4 次。您正在创建一个“空”分组,它将包装原始集合,以便您可以将集合“投影”到单个对象。由于您在“分组”上调用 4 个 linq 函数 - 原始列表将被迭代 4 次。它在功能上等同于:

var a = new
        {
            MinY = pts.Min(p => p.Y),
            MaxY = pts.Max(p => p.Y),
            MinX = pts.Min(p => p.X),
            MaxX = pts.Max(p => p.X)
        };

如果这对您来说是个问题,找到边界的惯用方法是使用 foreach 循环并手动跟踪最小和最大 x 和 y 坐标。这将是一个相对较短的函数,并将迭代次数减少 75%:

int MinX, MaxX, MinY, MaxY;
MaxX = MaxY = Int.MinValue;
MinX = MinY = Int.MaxValue;
foreach(Point p in pts)
{
    MinX = Math.Min(p.X, MinX);
    MaxX = Math.Max(p.X, MaxX);
    MinY = Math.Min(p.Y, MinY);
    MaxY = Math.Max(p.Y, MaxY);
}
var a = new
    {
        MinY,
        MaxY,
        MinX,
        MaxX
    };

可以使用Aggregate 循环查找带有 lambda 的最小值和最大值:

var a = pts.Aggregate(
     new {
        MinX = int.MaxValue,
        MaxX = int.MinValue,
        MinY = int.MaxValue,
        MaxY = int.MinValue
    },
    (acc, p) => new {
        MinX = Math.Min(p.X, acc.MinX);
        MaxX = Math.Max(p.X, acc.MaxX);
        MinY = Math.Min(p.Y, acc.MinY);
        MaxY = Math.Max(p.Y, acc.MaxY);
    });

但是聚合器将为源集合中的每个对象创建一个对象,并为“初始”对象加上一个。因此列表只会迭代一次,但会创建多个临时对象,从而增加需要进行 GC 处理的内存量。

【讨论】:

  • 现在这对我来说更有意义了。在我看来,它与您描述的完全一样——我将对其进行四次迭代。在此之前,我已经按照您的描述手动编写了 foreach 循环,但希望能有更...​​优雅的东西
  • 您说聚合器将“为每次迭代创建一个对象”。但是在使用聚合器时,有多少次迭代?
  • @Joe Aggregate 对每个项目进行一次迭代。所以一个完整的遍历。
  • 很公平。就我而言,如果我要调用 Aggregate() 而不是 Tuple,则使用值类型 System.ValueTuple 作为 Aggregate 的初始值和 lambda-return-value,这样可以最大限度地减少我的 GC 活动,不是吗? ValueTuple 不是引用类型
  • @Joe。它将为源集合中的每个项目创建一个对象(加上一个作为初始值)。源集合只有一次遍历。
【解决方案2】:

您在那里使用的方法至少对输入值进行了五次迭代(一次将它们“分组”,每个最小值/最大值一次),并且是一种非常奇怪的方式来处理您正在做的事情。

当您想要获取一组值并将它们压缩为一个值时,首选选择是.Aggregate(在其他语言中也称为reducefold)。

在您的情况下,您可以这样做。它应该只遍历你的集合一次:

public static (double minX, double maxX, double minY, double maxY) 
GetBounds(List<Point> pts)
{
    return pts.Aggregate(
        (Int32.MaxValue, Int32.MinValue, Integer.MaxValue, Int32.MinValue),
        (acc, point) => 
        (
            Math.Min(point.X, acc.Item1),
            Math.Max(point.X, acc.Item2),
            Math.Min(point.Y, acc.Item3),
            Math.Max(point.Y, acc.Item4)
        ));
}

【讨论】:

  • 是一种非常奇怪的方式 好吧,对于 L2O 来说这可能很奇怪(虽然我不会称之为极端),但对于 L2E 和类似的,它是非常标准的(也是唯一的) 在一次往返中进行多个聚合的方式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-04
  • 2014-07-10
相关资源
最近更新 更多