【问题标题】:How to use LINQ to compare values of sequential "neighbors" inside a List<>?如何使用 LINQ 比较 List<> 中顺序“邻居”的值?
【发布时间】:2011-05-06 08:13:02
【问题描述】:

看看这段代码:

        ColorResult contains Index, Color Name, and Probability

        Colors.Add(new ColorResult(1, "Unknown", 5f));
        Colors.Add(new ColorResult(2, "Blue", 80f));
        Colors.Add(new ColorResult(3, "Blue", 80f));
        Colors.Add(new ColorResult(4, "Green", 40f));
        Colors.Add(new ColorResult(5, "Blue", 80f));
        Colors.Add(new ColorResult(6, "Blue", 80f));
        Colors.Add(new ColorResult(7, "Red", 20f));
        Colors.Add(new ColorResult(8, "Blue", 80f));
        Colors.Add(new ColorResult(9, "Green", 5f));

使用 LINQ,您将如何完成以下任务:

1) 按顺序工作,当概率高于 60 的前两个项目具有相同的值(“未知”变为“蓝色" 因为 #2 和 #3 是蓝色的并且有 60+ 的概率)

2) 替换任何被四个具有相同值的邻居包围的概率低于 60 的项目(“绿色”变为“蓝色”,因为 #2、#3、#5 和 #6 是蓝色并且概率为 60 +)

3) 按顺序工作,替换 List 末尾的任何项目,这些项目前面有两个具有相同值的项目(与第一部分相同,但相反)。在示例数据中,#9 不会发生任何事情,因为 #7 需要是“蓝色”并且需要 60+ 概率。

使用循环这很容易,但我对如何在 LINQ 中比较顺序“邻居”感到非常困惑。

这是我对第 1 部分的原始解决方案:

        bool partOneCompleted = false;
        for (int i = 0; i < Colors.Count; i++)
        {
            if (Colors[i].ResultConfidence > 60)
            {
                // This item does not need to be changed
                partOneCompleted = true;
            }
            if (!partOneCompleted)
            {
                int twoItemsAway = i + 2;
                if (twoItemsAway < Colors.Count)
                {
                    if (Colors[twoItemsAway].Name == Colors[twoItemsAway - 1].Name && Colors[twoItemsAway].ResultConfidence > 60 && Colors[twoItemsAway - 1].ResultConfidence > 60)
                    {
                        // The next item, and the one after that both have the same value and 60+ confidence
                        for (int loopBack = i; loopBack >= 0; loopBack--)
                        {
                            Colors[loopBack].Name = Colors[twoItemsAway].Name;
                        }

                        partOneCompleted = true;
                    }
                }
            }
        }

任何 LINQ 专家可以分享最有效的实现吗?

【问题讨论】:

  • 所以您是在要求人们为您编写代码?如果你自己做了任何尝试,你应该发布它们,所以你看起来不那么懒惰:p
  • 至少分享这些“简单”的循环,这样人们就有了起点。
  • 我不认为 LINQ 在这里有任何快速的胜利。如果使用“常规循环”使代码更具可读性和易于理解,那么我会说这是前进的方向。
  • 我会发布伪代码,但它根本不使用 LINQ。我敢打赌,一定有比我现在做的更简单的方法。
  • 您改用 LINQ 的动机是什么?你真的只是说 IEnumerables 吗?您只需要五个元素的可见性即可实现所有这些规则,因此您可以编写一个方法,该方法预先读取两个元素,记住前两个元素,计算您的规则,然后生成固定序列中的下一个元素。

标签: c# .net linq lambda sequence


【解决方案1】:

这是第一个示例,让您开始。关键是使用Enumerable.Range,所以你有索引。如前所述,使用循环会更具可读性。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace ConsoleApplication1
{
    public class ColorResult
    {
        public int Index;
        public string Name;
        public float Prob;

        public ColorResult(int Index, string Name, float Prob)
        {
            this.Index = Index;
            this.Name = Name;
            this.Prob = Prob;
        }

        public override string ToString()
        {
            return Index.ToString() + ", " + Name + ", " + Prob.ToString();
        }
    }

    class Program
    {
        public static void Main()
        {
            List<ColorResult> Colors = new List<ColorResult>();

            Colors.Add(new ColorResult(1, "Unknown", 5f));
            Colors.Add(new ColorResult(2, "Blue", 80f));
            Colors.Add(new ColorResult(3, "Blue", 80f));
            Colors.Add(new ColorResult(4, "Green", 40f));
            Colors.Add(new ColorResult(5, "Blue", 80f));
            Colors.Add(new ColorResult(6, "Blue", 80f)); 
            Colors.Add(new ColorResult(7, "Red", 20f)); 
            Colors.Add(new ColorResult(8, "Blue", 80f));   
            Colors.Add(new ColorResult(9, "Green", 5f));


            var test1 = from i in Enumerable.Range(0, Colors.Count)
                        select (i < Colors.Count - 2 &&
                               (Colors[i].Prob < 60f) &&
                               (Colors[i + 1].Name == Colors[i + 2].Name) &&
                               (Colors[i+1].Prob > 60f) &&
                               (Colors[i+2].Prob > 60f)) ?
                        new ColorResult(1, Colors[i + 1].Name, Colors[i].Prob) :
                        Colors[i];


        }
    }
}

【讨论】:

  • 这是很好的代码,谢谢。我建议进行一项修改:一旦找到概率为 60+ 的项目,它应该停止。在您当前的代码中,它也替换了#4。总的来说,我开始同意你们的观点,即对于此类问题,循环可能比 LINQ 更好。
  • 在 LINQ 中执行和读取会非常尴尬(但可以使用子查询),for 循环肯定会更容易。
【解决方案2】:

我从第 1 部分的测试开始:

[Test]
public void Should_convert_leading_low_probability_colors()
{
    var colors = new List<ColorResult>
        {
            new ColorResult(1, "Unknown", 5f),
            new ColorResult(2, "Blue", 80f),
            new ColorResult(3, "Blue", 80f),
            new ColorResult(4, "Green", 40f),
            new ColorResult(5, "Blue", 80f),
            new ColorResult(6, "Blue", 80f),
            new ColorResult(7, "Red", 20f),
            new ColorResult(8, "Blue", 80f),
            new ColorResult(9, "Green", 5f)
        };

    ConvertLeadingLowProbabilityColors(colors);

    foreach (var colorResult in colors)
    {
        Console.WriteLine(colorResult.Index + " " + colorResult.Color);
    }

    colors[0].Color.ShouldBeEqualTo("Blue");
    colors[1].Color.ShouldBeEqualTo("Blue");
    colors[2].Color.ShouldBeEqualTo("Blue");
    colors[3].Color.ShouldBeEqualTo("Green");
    colors[4].Color.ShouldBeEqualTo("Blue");
    colors[5].Color.ShouldBeEqualTo("Blue");
    colors[6].Color.ShouldBeEqualTo("Red");
    colors[7].Color.ShouldBeEqualTo("Blue");
    colors[8].Color.ShouldBeEqualTo("Green");
}

和实施

private void ConvertLeadingLowProbabilityColors(IList<ColorResult> colors)
{
    var leadingBelow60 = Enumerable
        .Range(0, colors.Count)
        .TakeWhile(index => colors[index].Probability < 60)
        .ToList();
    if (leadingBelow60.Count > 0 && leadingBelow60.Count < colors.Count - 2)
    {
        int lastIndex = leadingBelow60.Last();
        var firstNext = colors[lastIndex + 1];
        var secondNext = colors[lastIndex + 2];
        if (firstNext.Probability > 60 &&
            secondNext.Probability > 60 &&
            firstNext.Color == secondNext.Color)
        {
            leadingBelow60.ForEach(index => colors[index].Color = firstNext.Color);
        }
    }
}

然后为第 3 部分添加了一个测试,因为它是第 1 部分的变体:

[Test]
public void Should_convert_trailing_low_probability_colors()
{
    var colors = new List<ColorResult>
        {
            new ColorResult(1, "Unknown", 5f),
            new ColorResult(2, "Blue", 80f),
            new ColorResult(3, "Blue", 80f),
            new ColorResult(4, "Green", 40f),
            new ColorResult(5, "Blue", 80f),
            new ColorResult(6, "Blue", 80f),
            new ColorResult(7, "Red", 20f),
            new ColorResult(8, "Blue", 40f),
            new ColorResult(9, "Green", 5f)
        };

    ConvertTrailingLowProbabilityColors(colors);

    foreach (var colorResult in colors)
    {
        Console.WriteLine(colorResult.Index + " " + colorResult.Color);
    }

    colors[0].Color.ShouldBeEqualTo("Unknown");
    colors[1].Color.ShouldBeEqualTo("Blue");
    colors[2].Color.ShouldBeEqualTo("Blue");
    colors[3].Color.ShouldBeEqualTo("Green");
    colors[4].Color.ShouldBeEqualTo("Blue");
    colors[5].Color.ShouldBeEqualTo("Blue");
    colors[6].Color.ShouldBeEqualTo("Blue");
    colors[7].Color.ShouldBeEqualTo("Blue");
    colors[8].Color.ShouldBeEqualTo("Blue");
}

和实施:

private void ConvertTrailingLowProbabilityColors(IList<ColorResult> colors)
{
    var trailingBelow60 = Enumerable
        .Range(0, colors.Count)
        .Select(i => colors.Count - 1 - i)
        .TakeWhile(index => colors[index].Probability < 60)
        .ToList();
    if (trailingBelow60.Count > 0 && trailingBelow60.Count < colors.Count - 2)
    {
        int lastIndex = trailingBelow60.Last();
        var firstPrevious = colors[lastIndex - 1];
        var secondPrevious = colors[lastIndex - 2];
        if (firstPrevious.Probability > 60 &&
            secondPrevious.Probability > 60 &&
            firstPrevious.Color == secondPrevious.Color)
        {
            trailingBelow60.ForEach(index => colors[index].Color = firstPrevious.Color);
        }
    }
}

然后,我处理了第 2 部分。我再次开始测试:

[Test]
public void Should_convert_surrounded_low_probability_colors()
{
    var colors = new List<ColorResult>
        {
            new ColorResult(1, "Unknown", 5f),
            new ColorResult(2, "Blue", 80f),
            new ColorResult(3, "Blue", 80f),
            new ColorResult(4, "Green", 40f),
            new ColorResult(5, "Blue", 80f),
            new ColorResult(6, "Blue", 80f),
            new ColorResult(7, "Red", 20f),
            new ColorResult(8, "Blue", 80f),
            new ColorResult(9, "Green", 5f)
        };

    ConvertSurroundedLowProbabilityColors(colors);

    foreach (var colorResult in colors)
    {
        Console.WriteLine(colorResult.Index + " " + colorResult.Color);
    }

    colors[0].Color.ShouldBeEqualTo("Unknown");
    colors[1].Color.ShouldBeEqualTo("Blue");
    colors[2].Color.ShouldBeEqualTo("Blue");
    colors[3].Color.ShouldBeEqualTo("Blue");
    colors[4].Color.ShouldBeEqualTo("Blue");
    colors[5].Color.ShouldBeEqualTo("Blue");
    colors[6].Color.ShouldBeEqualTo("Red");
    colors[7].Color.ShouldBeEqualTo("Blue");
    colors[8].Color.ShouldBeEqualTo("Green");
}

还有这个实现:

private void ConvertSurroundedLowProbabilityColors(IList<ColorResult> colors)
{
    var surrounding4Modification = new Surrounding4ModificationStrategy();
    foreach (int index in Enumerable
        .Range(0, colors.Count)
        .Where(index => surrounding4Modification.IsMatch(colors, index)))
    {
        surrounding4Modification.Update(colors, index);
    }
}

这一次拉出一个辅助类似乎更干净了:

public class Surrounding4ModificationStrategy
{
    public bool IsMatch(IList<ColorResult> input, int index)
    {
        if (index < 2)
        {
            return false;
        }
        if (index >= input.Count - 2)
        {
            return false;
        }
        if (input[index].Probability >= 60)
        {
            return false;
        }

        var secondPrevious = input[index - 2];
        if (secondPrevious.Probability < 60)
        {
            return false;
        }
        var firstPrevious = input[index - 1];
        if (firstPrevious.Probability < 60)
        {
            return false;
        }

        var firstNext = input[index + 1];
        if (firstNext.Probability < 60)
        {
            return false;
        }
        var secondNext = input[index + 2];
        if (secondNext.Probability < 60)
        {
            return false;
        }

        if (new[] { secondPrevious.Color, firstPrevious.Color, firstNext.Color, secondNext.Color }.Distinct().Count() > 1)
        {
            return false;
        }
        return true;
    }

    public void Update(IList<ColorResult> input, int index)
    {
        input[index].Color = input[index + 1].Color;
    }
}

最后,我用您的数据创建了一个综合测试:

[Test]
public void Should_convert_all_low_probability_colors()
{
    var colors = new List<ColorResult>
        {
            new ColorResult(1, "Unknown", 5f),
            new ColorResult(2, "Blue", 80f),
            new ColorResult(3, "Blue", 80f),
            new ColorResult(4, "Green", 40f),
            new ColorResult(5, "Blue", 80f),
            new ColorResult(6, "Blue", 80f),
            new ColorResult(7, "Red", 20f),
            new ColorResult(8, "Blue", 80f),
            new ColorResult(9, "Green", 5f)
        };

    ConvertLowProbabilityColors(colors);

    foreach (var colorResult in colors)
    {
        Console.WriteLine(colorResult.Index + " " + colorResult.Color);
    }

    colors[0].Color.ShouldBeEqualTo("Blue");
    colors[1].Color.ShouldBeEqualTo("Blue");
    colors[2].Color.ShouldBeEqualTo("Blue");
    colors[3].Color.ShouldBeEqualTo("Blue");
    colors[4].Color.ShouldBeEqualTo("Blue");
    colors[5].Color.ShouldBeEqualTo("Blue");
    colors[6].Color.ShouldBeEqualTo("Red");
    colors[7].Color.ShouldBeEqualTo("Blue");
    colors[8].Color.ShouldBeEqualTo("Green");
}

以及使用上面创建的方法的实现:

public void ConvertLowProbabilityColors(IList<ColorResult> colors)
{
    ConvertLeadingLowProbabilityColors(colors);
    ConvertSurroundedLowProbabilityColors(colors);
    ConvertTrailingLowProbabilityColors(colors);
}

如果这是我的代码库,我将继续围绕边缘情况添加测试,例如:第 1 部分和第 3 部分概率

【讨论】:

  • 感谢您的工作,这是完美的,也是 StackOverflow 如此强大的一个很好的例子。
【解决方案3】:

这是一个基于产量的解决方案,我猜这不是严格意义上的 LINQ,但它类似于 LINQ。快速实施;我确信有更好的方法来做到这一点。为窃取他的测试框架向 George 道歉(并 +1)。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace ConsoleApplication1
{
  public class ColorResult
  {
    public int Index;
    public string Name;
    public float Prob;

    public ColorResult(int Index, string Name, float Prob)
    {
      this.Index = Index;
      this.Name = Name;
      this.Prob = Prob;
    }

    public override string ToString()
    {
      return Index.ToString() + ", " + Name + ", " + Prob.ToString();
    }
  }

  class Program
  {
    // Iterate through the list remembering the last two elements
    // to implement rule 1
    public static IEnumerable<ColorResult> Rule1(IEnumerable<ColorResult> input)
    {
      ColorResult last2 = null;
      ColorResult last1 = null;
      foreach (var color in input)
      {
        if ((color.Prob < 60f)
            && (last1 != null) && (last1.Prob >= 60f)
            && (last2 != null) && (last2.Prob >= 60f)
            && (last2.Name == last1.Name))
        {
          color.Name = last2.Name;
        }
        yield return color;
        last2 = last1;
        last1 = color;
      }
    }

    // Iterate through the list with two element look-ahead
    // to implement rule 3
    public static IEnumerable<ColorResult> Rule3(IEnumerable<ColorResult> input)
    {
      ColorResult color = null;
      ColorResult ahead1 = null;
      foreach (var ahead2 in input)
      {
        if ((color != null) && (color.Prob < 60f)
            && (ahead1 != null) && (ahead1.Prob >= 60f)
            && (ahead2 != null) && (ahead2.Prob >= 60f)
            && (ahead1.Name == ahead2.Name))
        {
          color.Name = ahead1.Name;
        }
        yield return color;
        color = ahead1;
        ahead1 = ahead2;
      }
      // Using a null check here as a cheat way to test we've
      // actually had two inputs.
      // NB Will not preserve trailing nulls in the list;
      // you'll need to count inputs if you need that.
      if (color != null) yield return color;
      if (ahead1 != null) yield return ahead1;
    }

    public static void Main()
    {
      List<ColorResult> Colors = new List<ColorResult>();

      Colors.Add(new ColorResult(1, "Unknown", 5f));
      Colors.Add(new ColorResult(2, "Blue", 80f));
      Colors.Add(new ColorResult(3, "Blue", 80f));
      Colors.Add(new ColorResult(4, "Green", 40f));
      Colors.Add(new ColorResult(5, "Blue", 80f));
      Colors.Add(new ColorResult(6, "Blue", 80f));
      Colors.Add(new ColorResult(7, "Red", 20f));
      Colors.Add(new ColorResult(8, "Blue", 80f));
      Colors.Add(new ColorResult(9, "Green", 5f));

      var processed = Rule3(Rule1(Colors));
      foreach (var color in processed)
      {
        Console.WriteLine(color);
      }
    }
  }
}

【讨论】:

    【解决方案4】:

    首先,我相信规则 1 和 3 只是规则 2 的边缘情况。我将从 2 种扩展方法开始 LINQify:

    1. 一种为每个元素返回其周围邻居的方法。在开头和结尾添加两个空值以处理边缘情况。我把这个方法称为WithSurroundingNeigbours
    2. 一种从 5 个元素的列表中返回是否应排除中间元素的方法。我把它作为练习 :-)

    结合起来,这允许这样编写结果查询:

           var results = from x in Colors.WithSurroundingNeighbours()
                         where !x.ItemShouldBeRemoved()
                         select x[2]; 
    
    
    
    public static  class Extensions
    {
        public static IEnumerable<List<T>> WithSurroundingNeighbours<T>(this IEnumerable<T> input) where T : class
        {
            var q = new List<T>();
            var twoNulls = new T[] {null, null};
            foreach (var item in twoNulls.Concat(input).Concat(twoNulls))
            {
                q.Add(item);
                if (q.Count < 5) continue;
                yield return q;
                q.RemoveAt(0);
            }
        }
    
        public static bool ItemShouldBeRemoved(this List<ColorResult> items) 
        {
            if (items.Count != 5) throw new ArgumentException("expected list with exactly 5 items");
            // TODO: return true when Item3 of the tuple should be removed
            // for the first item in the list, Item1 and Item2 are null
            // for the last item in the list, Item4 and Item5 are null
            return false;
        } 
    }
    

    【讨论】:

      【解决方案5】:

      这是一个使用循环的完整解决方案,以防有人感兴趣:

              bool startComplete = false;
              for (int i = 0; i < Colors.Count; i++)
              {
                  if (Colors[i].ResultConfidence > 60)
                  {
                      // This item does not need to be changed
                      startComplete = true;
                  }
                  if (!startComplete)
                  {
                      int twoItemsAway = i + 2;
                      if (twoItemsAway < Colors.Count)
                      {
                          if (Colors[twoItemsAway].Name == Colors[twoItemsAway - 1].Name && Colors[twoItemsAway].ResultConfidence > 60 && Colors[twoItemsAway - 1].ResultConfidence > 60)
                          {
                              // The next item, and the one after that both have the same value and 60+ confidence
                              for (int loopBack = i; loopBack >= 0; loopBack--)
                              {
                                  Colors[loopBack].Name = Colors[twoItemsAway].Name;
                              }
      
                              startComplete = true;
                          }
                      }
                  }
              }
      
              bool endComplete = false;
              for (int i = Colors.Count - 1; i >= 0; i--)
              {
                  if (Colors[i].ResultConfidence > 60)
                  {
                      // This item does not need to be changed
                      endComplete = true;
                  }
                  if (!endComplete)
                  {
                      int twoItemsAway = i - 2;
                      if (twoItemsAway >= 0)
                      {
                          if (Colors[twoItemsAway].Name == Colors[twoItemsAway + 1].Name && Colors[twoItemsAway].ResultConfidence > 60 && Colors[twoItemsAway + 1].ResultConfidence > 60)
                          {
                              // The next item, and the one after that both have the same value and 60+ confidence
                              for (int loopForward = twoItemsAway; loopForward < Colors.Count; loopForward++)
                              {
                                  Colors[loopForward].Name = Colors[twoItemsAway].Name;
                              }
      
                              endComplete = true;
                          }
                      }
                  }
              }
      
              // Fill in the middle values.
      
              for (int i = 2; i < Colors.Count - 2; i++)
              {
                  if (Colors[i].ResultConfidence < 60)
                  {
                      int twoLeft = i - 2;
                      int oneLeft = i - 1;
                      int oneRight = i + 1;
                      int twoRight = i + 2;
      
                      if (Colors[twoLeft].Name == Colors[oneLeft].Name && Colors[oneLeft].Name == Colors[oneRight].Name && Colors[oneRight].Name == Colors[twoRight].Name
                          &&
                          Colors[twoLeft].ResultConfidence > 60 && Colors[oneLeft].ResultConfidence > 60 && Colors[oneRight].ResultConfidence > 60 && Colors[twoRight].ResultConfidence > 60)
                      {
                          Colors[i].Name = Colors[oneRight].Name;
                      }
      
                  }
              }
      

      【讨论】:

        【解决方案6】:

        您可以使用 linq 非常巧妙地对顺序项进行操作:

          var colours = new List<string>(new[]{"red", "green", "blue"});
          var added = colours
                      .Skip(1)
                      .Zip(colours,
                      (second, first) => first + second);
          foreach (var colour in added)
          {
            Console.WriteLine(colour);
          } 
        

        请注意,我们跳过了序列的第一个元素,然后将结果与自身一起压缩。 这给出了:

        红绿

        绿蓝

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-10-05
          • 2012-06-07
          • 1970-01-01
          • 2014-11-25
          • 1970-01-01
          • 1970-01-01
          • 2023-03-12
          相关资源
          最近更新 更多