如何将一个IEnumerable与自身压缩?

7
我正在实现一些基于点列表的数学算法,如距离、面积、重心等。就像在这篇文章中所描述的那样:Find the distance required to navigate a list of points using linq
该文章描述了如何通过将序列“与自身”进行压缩,并通过将原始IEnumerable的起始位置偏移1来生成Zip序列,从而计算一系列点(按顺序取)的总距离。
因此,假设使用.Net 4.0中的Zip扩展,点类型为Point,并且有一个合理的Distance公式,您可以调用以下函数来生成从一个点到下一个点的距离序列,然后对距离求和:
var distances = points.Zip(points.Skip(1),Distance);
double totalDistance = distances.Sum();

区域和重心计算类似,需要迭代序列,处理每一对点(points[i] 和 points[i+1])。我考虑创建一个通用的 IEnumerable 扩展方法,适用于实现这些(可能还有其他)基于序列、每次取两个项目(points[0] 和 points[1],points[1] 和 points[2],…,points[n-1] 和 points[n](还是 n-2 和 n-1 …)),并应用函数的算法。

我的通用迭代器与 Zip 类似,但它不会接收第二个要进行 zip 的序列,因为它实际上只会与自己进行 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,代码如下:

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。
还有一个关于重心的维基百科文章的链接。 如果感兴趣,请前往维基百科并搜索重心。
刚开始,因此没有足够的声望发布多个链接。
开始编辑
为了完整起见,如果有人在搜索距离,面积或重心之后到达这里,以下是我接受位置类型列表(假定为面积和重心关闭)并返回位置的距离(沿着),面积和重心的函数:
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);
  }
}

欢迎发表评论。

编辑结束

2个回答

8
此外,是否有更好的名称来反映它是一个众所周知的模式(如果确实是众所周知的模式)?
是的 - 它也被称为 "Pairwise"。例如,这里做过类似的事情:这里。此前也有过一个关于它的问题SO上的这里
现在可以使用 .NET 4.0 的 Zip 来实现 Pairwise。对于 LINQ to Objects 解决方案来说,这似乎是一个合理的方法,尽管在这一点上,拥有一个适用于 .NET v3.5 的版本可能对更广泛的受众更有用。

我接受这个答案,主要是因为涉及到Pairwise术语并链接到了Pairwise参考文献。虽然我在StackOverflow上看到过Pairwise的链接,但在搜索此问题时没有遇到过。我想指出的是,就像我回复Gideon Engelberth时所述,实现我上述描述的方式确实会导致输入的IEnumerable被迭代两次,这可能会很昂贵,具体取决于该IEnumerable或上游IEnumerable生成答案所需的操作。 - wageoghe

3
当我做类似的事情时,我称之为“SelectWithPrevious”,并且有一个版本,它具有“SelectWithPreviousItem”(使用“Func<TSource, TSource, TResult>”)和“SelectWithPreviousResult”(使用“Func<TResult, TSource, TResult>”)两个重载。此外,我通过直接存储最后一个元素来实现它,而不是像Zip方法那样迭代序列两次。我从未使用过LINQ-to-SQL,但我想知道Zip/Skip方法是否会对服务器进行两次查询以评估查询两次。

这是一个关于Zip/Skip是否需要多次访问服务器的好问题,我不知道答案。在我的情况下,这些操作通常(总是?)针对对象执行。我编写了一个简单的测试,使用硬编码的yield return语句对我的ZipMyself扩展进行了测试。由于两个序列并行迭代,它会命中每个yield return两次。当我使用Pairwise迭代器直接使用枚举器进行测试时,它会命中每个yield return一次,因为我实际上只迭代了一次序列。 - wageoghe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接