【问题标题】:How to Zip one IEnumerable with itself如何用自己压缩一个 IEnumerable
【发布时间】:2011-02-15 15:57:41
【问题描述】:

我正在实现一些基于点列表的数学算法,例如距离、面积、质心等。就像在这篇文章中一样:Find the distance required to navigate a list of points using linq

那篇文章描述了如何计算点序列的总距离(按顺序排列),方法是通过“自身”压缩序列,通过将原始 IEnumerable 的起始位置偏移 1 来生成 Zip 序列。

因此,给定 .Net 4.0 中的 Zip 扩展,假设点类型为 Point,以及一个合理的距离公式,您可以进行这样的调用以生成从一个点到下一个点的距离序列,然后将距离:

var distances = points.Zip(points.Skip(1),Distance);
double totalDistance = distances.Sum();

面积和质心计算的相似之处在于它们需要遍历序列,处理每对点(points[i] 和 points[i+1])。我想制作一个通用的 IEnumerable 扩展,适用于实现对序列进行操作的这些(可能还有其他)算法,一次获取两个项目(点 [0] 和点 [1],点 [1] 和点 [2], ...,points[n-1] 和 points[n](或者是 n-2 和 n-1 ...)并应用函数。

我的通用迭代器将具有与 Zip 相似的签名,但它不会收到要压缩的第二个序列,因为它实际上只是自己压缩。

我的第一次尝试是这样的:

public static IEnumerable<TResult> ZipMyself<TSequence, TResult>(this IEnumerable<TSequence> seq, Func<TSequence, TSequence, TResult> resultSelector)
{
  return seq.Zip(seq.Skip(1),resultSelector);
}

开始编辑: 看到响应后,我已经实现了 Pairwise,并像这样显式使用了底层 Enumerator:

public static IEnumerable<TResult> Pairwise<TSequence, TResult>(this IEnumerable<TSequence> seq, Func<TSequence, TSequence, TResult> resultSelector)
{
  TSequence prev = default(TSequence);
  using (IEnumerator<TSequence> e = seq.GetEnumerator())
  {
    if (e.MoveNext()) prev = e.Current;

    while (e.MoveNext()) yield return resultSelector(prev, prev = e.Current);
  }
}

虽然肯定比我的初始版本更复杂,但这个迭代输入序列一次,而原来的迭代两次。

结束编辑

有了我的通用迭代器,我可以编写如下函数:

public static double Length(this IEnumerable<Point> points)
{
  return points.ZipMyself(Distance).Sum();
}

然后这样称呼它:

double d = points.Length();

double GreensTheorem(Point p1, Point p1)
{
  return p1.X * p2.Y - p1.Y * p2.X;
}

public static double SignedArea(this IEnumerable<Point> points)
{
  return points.ZipMyself(GreensTheorem).Sum() / 2.0
}

public static double Area(this IEnumerable<Point> points)
{
  return Math.Abs(points.SignedArea());
}

public static bool IsClockwise(this IEnumerable<Point> points)
{
  return SignedArea(points) < 0;
}

然后这样称呼他们:

double a = points.Area();
bool isClockwise = points.IsClockwise();

在这种情况下,是否有任何理由不使用 Zip 和 Skip(1) 实现“ZipMyself”? LINQ 中是否已经有一些东西可以自动执行此操作(使用自身压缩列表) - 并不是说​​它需要变得更容易;-)

此外,是否有更好的扩展名可以反映它是一种众所周知的模式(如果它确实是一种众所周知的模式)?

这里有一个关于面积计算的 StackOverflow 问题的链接。问题2432428。

还有一个指向 Wikipedia 关于 Centroid 的文章的链接。如果有兴趣,只需访问 Wikipedia 并搜索 Centroid。

刚开始,所以没有足够的代表发布多个链接。

开始编辑

为了完整起见,如果有人在搜索距离、面积或质心后到达这里,这里是我的函数,它们接受位置类型列表(假设区域和质心是封闭的)并返回距离(沿)、面积和位置的质心:

public struct Position
{
  public double X;
  public double Y;

  static public double Distance(Position p1, Position p2)
  {
    double dx = p2.X - p1.X;
    double dy = p2.Y - p1.Y;
    return Math.Sqrt(dx*dx + dy*dy);
  }
}

public static class PointMath
{
  public static double Distance(IEnumerable<Position> pts)
  {
    return pts.Pairwise((p1, p2) => Position.Distance(p1, p2)).Sum();
  }

  private static bool IsClockwise(IEnumerable<Position> pts)
  {
    return SignedArea(pts) < 0;
  }

  private static double SignedArea(IEnumerable<Position> pts)
  {
    return pts.Pairwise((p1, p2) => (p1.X * p2.Y - p1.Y * p2.X)).Sum() / 2.0;
  }

  public static double Area(IEnumerable<Position> pts)
  {
    return Math.Abs(SignedArea(pts));
  }

  public static Position Centroid(IEnumerable<Position> pts)
  {
    double a = SignedArea(pts);

    var  c = pts.Pairwise((p1, p2) => new 
                                      { 
                                        x = (p1.X + p2.X) * (p1.X * p2.Y - p2.X * p1.Y), 
                                        y = (p1.Y + p2.Y) * (p1.X * p2.Y - p2.X * p1.Y)   
                                      })
                .Aggregate((t1, t2) => new 
                                       { 
                                         x = t1.x + t2.x, 
                                         y = t1.y + t2.y 
                                       });

    return new Position(1.0 / (a * 6.0) * c.x, 1.0 / (a * 6.0) * c.y);
  }
}

欢迎评论。

结束编辑

【问题讨论】:

    标签: c# .net linq algorithm geometry


    【解决方案1】:

    此外,是否有更好的扩展名可以反映它是一种众所周知的模式(如果它确实是一种众所周知的模式)?

    是的 - 它也被称为Pairwise。之前已经做过,例如here。在here on SO之前也有一个问题。

    正如您所指出的,现在可以根据 .NET 4.0 的 Zip 实现 Pairwise。对于 LINQ to Objects 解决方案来说,这似乎是一种合理的方法,尽管此时拥有适用于 .NET v3.5 的版本可能对更广泛的受众更有用。

    【讨论】:

    • 我接受这个作为答案,主要是因为对于术语 Pairwise 和链接到 Pairwise 引用。我在 StackOverflow 上看到了 Pairwise 链接,但在搜索此问题时没有遇到它。我会在这里指出,正如我在回复 Gideon Engelberth 时所做的那样,实现我上面描述的方式确实会导致输入 IEnumerable 被迭代两次,我认为这可能会很昂贵,具体取决于 IEnumerable 或上游的必须做什么生成答案。
    【解决方案2】:

    当我做了类似的事情时,我将其命名为 SelectWithPrevious,并且有一个版本对“SelectWithPreviousItem”(使用 Func&lt;TSource, TSource, TResult&gt;)和“SelectWithPreviousResult”(使用 Func&lt;TResult, TSource, TResult&gt;)都有重载。

    另外,我通过直接存储最后一个元素而不是像 Zip 方法那样迭代序列两次来实现它。由于从未使用过 LINQ-to-SQL,我不能肯定地说,但我想知道 Zip/Skip 方法是否会两次访问服务器以对查询进行两次评估。

    【讨论】:

    • 这是一个关于 Zip/Skip 多次访问服务器的好问题,但我不知道答案。就我而言,这些操作通常(总是?)对对象执行。我针对带有硬编码的 yield return 语句的序列编写了一个简单的 ZipMyself 扩展测试。因为这两个序列是并行迭代的,所以它会在每个收益回报中命中两次。当我按照您的描述直接使用 Enumerator 使用 Pairwise 迭代器进行测试时,它会命中每个 yield return 一次,因为我实际上只迭代了一次序列。
    猜你喜欢
    • 2010-10-15
    • 1970-01-01
    • 2015-03-12
    • 2011-04-28
    • 2011-01-04
    • 2015-03-27
    • 2013-12-25
    • 2017-09-22
    相关资源
    最近更新 更多