【问题标题】:IEnumerable - Return items in range either side of elementIEnumerable - 返回元素任一侧范围内的项目
【发布时间】:2011-08-22 12:06:50
【问题描述】:

我需要从 IEnumerable 中获取一个元素,然后返回它自己和两边的一系列元素。

所以,是这样的:

var enumerable = new[] {54, 107, 24, 223, 134, 65, 36, 7342, 812, 96, 106};
var rangeSize = 2;
var range = enumerable.MySelectRange(x => x == 134, rangeSize);

会返回类似{ 24, 223, 134, 65, 36 }

(本项目使用.Net 3.5)

编辑 好的,人们似乎对整数数组感兴趣。 我已经更改了示例,希望能更清楚地说明我的目标。

请记住,这不一定是IEnumerable<int>,但实际上是IEnumerable<TSomething>

【问题讨论】:

  • Jon Skeet 实现了某种 SmartEnumerable,您可以使用它使用索引进行枚举,因此您可以将索引限制在指定值 +/- 范围内。这只是一个想法..看看。 msmvps.com/blogs/jon_skeet/archive/2007/07/27/…
  • 或者您可以简单地使用 for 循环并管理索引。
  • 如果范围中心出现不止一次(即 5 次)怎么办?您应该返回第一个窗口还是所有窗口?
  • @digEmAll: 好问题......在当前的实现中没有重复的机会,但如果我想让它成为一个通用的解决方案,我需要牢记这一点。
  • @Kornelije Petak:是的,我想过,但不能在 LINQ 中做到这一点很烦人 :)

标签: c# linq ienumerable


【解决方案1】:

此扩展方法查找序列中满足给定谓词的第一个元素,然后返回该元素及其一定数量的相邻元素。它处理最终情况。

public static IEnumerable<T> FirstAndNeighbours<T>(
  this IEnumerable<T> source,
  Func<T,bool> predicate,
  int numOfNeighboursEitherSide)
{
  using (var enumerator = source.GetEnumerator())
  {
    var precedingNeighbours = new Queue<T>(numOfNeighboursEitherSide);
    while(enumerator.MoveNext())
    {
      var current = enumerator.Current;
      if (predicate(current))
      {
        //We have found the first matching element. First, we must return
        //the preceding neighbours.
        foreach (var precedingNeighbour in precedingNeighbours)
          yield return precedingNeighbour;

        //Next, return the matching element.
        yield return current;

        //Finally, return the succeeding neighbours.
        for (int i = 0; i < numOfNeighboursEitherSide; ++i)
        {
          if (!enumerator.MoveNext())
            yield break;

          yield return enumerator.Current;
        }
        yield break;
      }
      //No match yet, keep track of this preceding neighbour.
      if (precedingNeighbours.Count >= numOfNeighboursEitherSide)
        precedingNeighbours.Dequeue();
      precedingNeighbours.Enqueue(current);
    }
  }
}

【讨论】:

  • 这对我来说看起来很棒......刚刚测试过,它似乎运行良好。谢谢马特!
【解决方案2】:

假设你可以获得中间元素的索引:

var enumerable = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

int range = 2;
int index = 10;

enumerable.Skip(index-range).Take(range)
.Union(enumerable.Skip(index).Take(1))
.Union(
    enumerable.Skip(index+1).Take(range)
).Dump();

Dump() 调用是针对LinqPad

编辑:

感谢Gabe的评论,去掉了多余的两个Skip()/Take()

enumerable.Skip((index < range) ? 0 : index-range)
          .Take(((index < range) ? index : range) + range + 1)
          .Dump();

【讨论】:

  • 你到底为什么要枚举 3 次而不是只做 Take(2 * range + 1)
  • 因为当索引接近下限(0
  • 你认为将整个序列迭代 3 次比几个 if 语句更好?
  • 不,这只是另一种方式(也许更具可读性)。无论如何,为什么序列要迭代三遍(谈论编辑版本)?
  • 每次运行Skip,它都必须从头开始枚举整个序列。例如,如果序列是数据库查询的结果,则必须重新查询数据库。
【解决方案3】:

编辑:由于问题更新而更新了答案**

一个扩展方法应该可以做到,现在支持任何类型T

public static IEnumerable<T> Range<T>(this IEnumerable<T> enumerable, Func<T,bool> selector, int size)
{
    Queue<T> queue = new Queue<T>();
    bool found = false;
    int count = 0;
    foreach(T item in enumerable)
    {
            if(found)
            {
                if(count++ < size)
                {
                    yield return item;
                }
                else
                {
                    yield break;
                }
            }
            else
            {
                if(queue.Count>size)
                    queue.Dequeue();

                if(selector(item))
                {
                    found = true;
                    foreach(var stackItem in queue)
                        yield return stackItem;

                    yield return item;


                }
                else
                {
                    queue.Enqueue(item);
                }
            }
        }

使用量接近您的要求

 var enumerable = new[] {54, 107, 24, 223, 134, 65, 36, 7342, 812, 96, 106};
 Console.WriteLine(String.Join(",",enumerable.ToArray()));
 var rangeSize = 2;
 var range = enumerable.Range((x) => x == 134, rangeSize);
 Console.WriteLine(String.Join(",",range.ToArray()));

现场示例:http://rextester.com/rundotnet?code=ACKDD76841

【讨论】:

  • 好吧,这看起来很有希望......你能想出一种方法让它与 IEnumerable 一起工作吗?实际使用不会只是一个整数数组!
  • 这甚至不接近 OP 想要的!您正在查看 Enumerable 中的所有值,并返回所有加/减 sizerangeMiddle 的值。 OP想要的是找到满足条件的第一个项目并返回rangeSize先验和rangeSize连续元素。
  • @NeilD - 现在更新支持任何 T,你是新的要求
【解决方案4】:

以下给出了非线性序列的正确答案并且是有效的,例如:

const int PivotValue = 5;
const int RangeSize = 2;

int[] enumerable = new[] { 0, 1, 2, 3, 4, 5, 600, 700, 800, 900, 1000 };

IEnumerable<int> range = enumerable.PivotRange(PivotValue, RangeSize);}

//3, 4, 5, 600, 700.

代码 - 通用版本

public static IEnumerable<T> PivotRange<T>(
    this IEnumerable<T> source, T pivot, int size) where T : IComparable<T>
{
    T[] left = new T[size];
    int lCount = 0, rCount = 0;
    IEnumerator<T> enumerator = source.GetEnumerator();

    while (enumerator.MoveNext())
    {
        T item = enumerator.Current;

        if (item.CompareTo(pivot) == 0)
        {
            int start = lCount > size ? lCount % size : 0;
            int end = Math.Min(size, lCount);

            for (int i = start; i < start + end; i++)
                yield return left[i % size];

            yield return pivot;

            while (enumerator.MoveNext() && rCount++ < size)
                yield return enumerator.Current;

            break;
        }

        if (size <= 0) continue;

        left[lCount++ % size] = item;
    }
}

更新 - 单元测试

[Test]
public void Linear()
{
    const int PivotValue = 5;
    const int RangeSize = 2;

    int[] enumerable = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 3, 4, 5, 6, 7 }, range);
}

[Test]
public void NonLinear()
{
    const int PivotValue = 5;
    const int RangeSize = 2;

    int[] enumerable = new[] { 0, 1, 2, 3, 4, 5, 600, 700, 800, 900, 1000 };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 3, 4, 5, 600, 700 }, range);
}

[Test]
public void NoLeft()
{
    const int PivotValue = 5;
    const int RangeSize = 2;

    int[] enumerable = new[] { 5, 600, 700, 800, 900, 1000 };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 5, 600, 700 }, range);
}

[Test]
public void NoRight()
{
    const int PivotValue = 5;
    const int RangeSize = 2;

    int[] enumerable = new[] { 0, 1, 2, 3, 4, 5 };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 3, 4, 5 }, range);
}

[Test]
public void ZeroRange()
{
    const int PivotValue = 5;
    const int RangeSize = 0;

    int[] enumerable = new[] { 0, 1, 2, 3, 4, 5 };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 5 }, range);
}

[Test]
public void LeftShorterThanRange()
{
    const int PivotValue = 5;
    const int RangeSize = 2;

    int[] enumerable = new[] { 4, 5, 6, 7, 8 };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 4, 5, 6, 7 }, range);
}

[Test]
public void RightShorterThanRange()
{
    const int PivotValue = 5;
    const int RangeSize = 2;

    int[] enumerable = new[] { 2, 3, 4, 5, 6, };

    int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();

    CollectionAssert.AreEqual(new[] { 3, 4, 5, 6 }, range);
}

【讨论】:

  • 这不应仅限于int,它不起作用。我在for 循环中遇到异常。
  • @Gabe 已转换为通用 - 没有看到问题编辑。寻找 int 异常...
  • @Gabe 我为常见情况添加了一些基本单元测试。什么顺序导致了您的异常?
  • 我在 OP 的测试序列中遇到了异常。
  • @Gabe 我刚刚查看了我的第一篇文章并测试了代码。我的原始帖子和 OP 的测试序列也不例外。不知道你从哪里得到的?编辑历史在那里......“它不起作用”有点诅咒我的帖子。我修复的唯一例外情况是当 rangeSize 为零时,您的解决方案也有例外。
【解决方案5】:

你可以这样做。

var enumerable = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var rangeSize = 2;
var index = enumerable.FirstOrDefault(x=> x == 2);
var range = enumerable.Skip(index + 1).Take(5);

【讨论】:

    【解决方案6】:

    只要元素是独一无二的,下面的功能就会为你工作。

    public static IEnumerable<T> MySelectRange<T>(this IEnumerable<T> me, Func<T, bool> pred, int range)
    {
          var first = me.TakeWhile(i => !pred(i)).TakeLast(range);
          var second = me.SkipWhile(i => !pred(i)).Take(range + 1);
          return first.Concat(second);
    }
    

    注意:TakeLast 似乎来自 Rx 库

    【讨论】:

      【解决方案7】:

      可以这么简单:

      public IEnumerable<T> GetRange<T>(IEnumerable<T> enumerable, int rangeSize, T value)
          {
              for (int i = 0; i < enumerable.Count(); ++i)
              {
                  if (enumerable.ElementAt(i).Equals(value))
                  {
                      for (int j = Math.Max(0, i - rangeSize); j < Math.Min(i + rangeSize + 1, enumerable.Count()); ++j)
                      {
                          yield return enumerable.ElementAt(j);
                      }
                  }
              }
          }
      }
      

      【讨论】:

      • 虽然这看起来应该可行,但如果您打算将它视为一个列表,则应该将它具体化为一个列表。就目前而言,您每次迭代都会使用CountElementAt 多次枚举列表。
      • @Gideon 同意,这在大型列表中表现不佳,它会成倍恶化。但是,将整个输入类型具体化会破坏基于流的枚举性质。
      【解决方案8】:

      这是一个基于队列的版本,只有我实现了自己的队列(不像其他使用Queue 的队列)。实现我自己的队列的好处是它的效率稍微高一点。

      public static IEnumerable<T> MySelectRange<T>(this IEnumerable<T> source,
                                                    Func<T, bool> selector,
                                                    int rangeSize)
      {
          var firstN = new T[rangeSize];
          int pos = 0;
          bool found = false;
      
          foreach (T item in source)
          {
              if (found)
                  if (pos++ <= rangeSize)
                      yield return item;
                  else
                      break;
              if (selector(item))
              {
                  found = true;
                  for (int i = Math.Max(0, pos - rangeSize); i < pos; i++)
                      yield return firstN[i % rangeSize];
                  yield return item;
                  pos = 0;
              }
              else if (rangeSize > 0)
                  firstN[pos++ % rangeSize] = item;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2011-11-16
        • 1970-01-01
        • 1970-01-01
        • 2016-06-10
        • 1970-01-01
        • 2019-06-07
        • 1970-01-01
        • 1970-01-01
        • 2015-09-12
        相关资源
        最近更新 更多