【问题标题】:What is the most elegant way to get a set of items by index from a collection?从集合中按索引获取一组项目的最优雅方法是什么?
【发布时间】:2010-11-04 08:11:24
【问题描述】:

给定

IList<int> indexes;
ICollection<T> collection;

根据indexes中提供的索引提取collection中所有T的最优雅的方法是什么?

例如,如果集合包含

"Brian", "Cleveland", "Joe", "Glenn", "Mort"

并且索引包含

1, 3

回报是

"Cleveland," "Glenn"

编辑:假设 indexes 始终按升序排序。

【问题讨论】:

  • @Bob,你要求优雅。 Eric 的回答是正确的并且超级快,但是以 5 * } ( } } } } } 结尾的方法并不是超级优雅
  • @Sam,来自 google 定义:优雅 - “在外观或行为或风格上精致而有品位”我发现 Eric 的回答在其行为方式上很有品位。花括号不打扰我。我还发现解决问题的方式很简单。

标签: c#


【解决方案1】:

这假定索引序列是非负索引的单调升序。策略很简单:对于每个索引,将集合上的枚举器提升到该点并产生元素。

public static IEnumerable<T> GetIndexedItems<T>(this IEnumerable<T> collection, IEnumerable<int> indices)
{
    int currentIndex = -1;
    using (var collectionEnum = collection.GetEnumerator())
    {
        foreach(int index in indices)
        {
            while (collectionEnum.MoveNext()) 
            {
                currentIndex += 1;
                if (currentIndex == index)
                {
                    yield return collectionEnum.Current;
                    break;
                }
            }
        }    
    }
}

此解决方案相对于其他解决方案的优势已发布:

  • 额外存储 O(1) - 其中一些解决方案在空间中是 O(n)
  • O(n) 时间 - 其中一些解决方案是时间二次方
  • 适用于任意两个序列;不需要 ICollection 或 IList。
  • 只迭代一次集合;一些解决方案会多次迭代集合(例如,从中构建一个列表。)

缺点:

  • 更难阅读

【讨论】:

  • 看起来也与我的解决方案非常相似。为什么选择在外循环中枚举索引?
  • 您的解决方案利用了索引在列表中这一事实;我的没有。如果您从索引位于可索引列表中并且集合是不可索引序列的前提开始,那么让您的外部循环遍历集合并使用索引“枚举”列表是有意义的。但是,如果您从两者都是不可索引序列的前提开始,那么从控制流的角度来看,哪个循环“超出”另一个循环并不重要。
  • 但还有另一个方面。在您的解决方案中,您有一个“元索引”——代码的读者需要了解 indexIndex 是索引列表中的一个索引,这可能是一个令人困惑的“级别混合”。在我的解决方案中,我选择确保卡在 currentIndex 中的“索引”实际上是集合中的索引,而不是集合中索引的索引。这是一个微妙的观点,但我认为这是一个很好的观点。
  • 是的——它使代码更难阅读。作为作者,我没有看到这一点,因为我知道 indexedIndex 是什么......但是基本思想非常相似 - 我的算法也具有您指出的所有优点。和缺点.. :) 对我来说,实现的想法是最重要的——无论你是做一个收益返回还是一个扩展方法都没有那么重要。
  • 为什么有人认为“a++; if (a == b)” 比“if (++a == b)” 更难阅读,这超出了我的理解。我有两个逻辑操作——一个递增的副作用和一个有条件的 goto。将它们组合成一个语句比将它们单独的语句更容易混淆。每个语句应该有一个任务。不是两个。
【解决方案2】:

这是一个更快的版本:

IEnumerable<T> ByIndices<T>(ICollection<T> data, IList<int> indices)
{
    int current = 0;
    foreach(var datum in data.Select((x, i) => new { Value = x, Index = i }))
    {
        if(datum.Index == indices[current])
        {
            yield return datum.Value;
            if(++current == indices.Count)
                yield break;
        }
    }
}

【讨论】:

  • 只有在对索引列表进行排序时才能正常工作,但这是规范的一部分,因此您会获得支持;)
  • 是的——如果没有对索引列表进行排序,那么在不对它进行排序或复制集合的情况下可能没有任何方法可以快速做到这一点。
  • 与我的版本非常相似...除了 .Select 和自动索引。我想知道哪个版本的性能更高。
  • 可能是你的。以我的经验,与“功能等效”的命令式代码相比,LINQ IEnumerable 旋转通常会有一点开销。
【解决方案3】:

不知道这有多优雅,但给你。

由于ICollection&lt;&gt; 没有为您提供索引,因此我只使用了IEnumerable&lt;&gt;,并且由于我不需要IList&lt;&gt; 上的索引,因此我也使用了IEnumerable&lt;&gt;

public static IEnumerable<T> IndexedLookup<T>(
    IEnumerable<int> indexes, IEnumerable<T> items)
{
    using (var indexesEnum = indexes.GetEnumerator())
    using (var itemsEnum = items.GetEnumerator())
    {
        int currentIndex = -1;
        while (indexesEnum.MoveNext())
        {
            while (currentIndex != indexesEnum.Current)
            {
                if (!itemsEnum.MoveNext())
                    yield break;
                currentIndex++;
            }

            yield return itemsEnum.Current;
        }
    }
}

编辑:刚刚注意到我的解决方案类似于 Erics。

【讨论】:

    【解决方案4】:

    我会使用扩展方法

    public static IEnumerable<T> Filter<T>(this IEnumerable<T> pSeq, 
                                           params int [] pIndexes)
    {
          return pSeq.Where((pArg, pId) => pIndexes.Contains(pId));
    }
    

    【讨论】:

    • 首先,索引在列表中,而不是数组中。其次,如果索引列表很长,这是非常低效的。
    • 通常使用 .Contains,如果您不知道集合中有多少索引,请不要这样做。或者你会走 O(n^2) 的艰难道路。如果您想安全起见,则应使用中间查找/字典/哈希集并在此集合上进行测试,而不是在普通列表上进行测试(线性搜索对您不利)
    • 我的解决方案提供了更快的答案。
    • public static IEnumerable Filter(this IEnumerable pSeq, IDictionary 索引) { return pSeq.Where((pArg, pId) => indices.Contains (pId));像这样调用 collection.Filter(indexes.ToDictionary(n => n));使用 hashset 会更好(使用更少的内存),但您需要一个特定的 ToHashset 扩展方法。
    • 嗯。让它成为一个正确的答案。
    【解决方案5】:

    你可以在扩展方法中做到这一点:

    static IEnumerable<T> Extract<T>(this ICollection<T> collection, IList<int> indexes)
    {
       int index = 0;
       foreach(var item in collection)
       {
         if (indexes.Contains(index))
           yield item;
         index++;
       }
    }
    

    【讨论】:

    • 如果优雅意味着“易于理解”,这将获胜......但是,如果索引列表很大, Contains() 会损害性能。
    • 您使用的只是Contains 上的indexes,所以最好通过接受IEnumerable&lt;int&gt; 来放松。
    【解决方案6】:

    不优雅,但高效 - 确保索引排序...

    ICollection<T> selected = new Collection<T>();
    var indexesIndex = 0;
    var collectionIndex = 0;
    foreach( var item in collection )
    {
        if( indexes[indexesIndex] != collectionIndex++ )
        {
            continue;
        }
        selected.Add( item );
        if( ++indexesIndex == indexes.Count )
        {
            break;
        }
    }
    

    【讨论】:

      【解决方案7】:

      作为一个正确的答案:

      var col = new []{"a","b","c"};
      var ints = new []{0,2};
      var set = new HashSet<int>(ints);
      
      var result = col.Where((item,index) => set.Contains(index));
      

      通常使用 IList.Contains 或 Enumerable.Contains,如果您不知道集合中有多少索引,请不要在列表中进行查找。或者你会走 O(n^2) 的艰难道路。如果您想安全起见,您应该使用中间查找/字典/哈希集并在此集合上进行测试,而不是在普通列表上进行测试(线性搜索对您不利)

      【讨论】:

      • 我喜欢你使用 Hashset 的想法
      【解决方案8】:

      这里已经有几个很好的建议了,我只需要投入两分钱。

      int counter = 0;
      var x = collection
          .Where((item, index) => 
              counter < indices.Length && 
              index == indices[counter] && 
              ++counter != 0);
      

      编辑:是的,第一次没想到。只有在满足其他两个条件时才会发生增量..

      【讨论】:

      • 如果条件中的子句是相反的,这可能会起作用——照原样,即使没有找到结果,计数器也会增加,造成严重破坏。
      【解决方案9】:

      我发现这个解决方案特别优雅,而且更容易理解。

      解决方案 1

         public static IEnumerable<T> GetIndexedItems2<T>(this IEnumerable<T> collection,    IEnumerable<int> indices) {
      
              int skipped = 0;
              foreach (int index in indices) {
                  int offset = index - skipped;
                  collection = collection.Skip(offset);
                  skipped += offset;
                  yield return collection.First();
              }
          }
      

      这可以进一步重构为一个真正简单的实现:

      解决方案 2

         public static IEnumerable<T> GetIndexedItems3<T>(this IEnumerable<T> collection, IEnumerable<int> indices) {
              foreach (int offset in indices.Distances()) {
                  collection = collection.Skip(offset);
                  yield return collection.First();
              }
          }
      
          public static IEnumerable<int> Distances(this IEnumerable<int> numbers) {
              int offset = 0;
              foreach (var number in numbers) {
                  yield return number - offset;
                  offset = number;
              }
          }
      

      但我们还没有完成

      由于延迟执行,LINQ 跳过太慢了。

         public static IEnumerable<T> GetIndexedItems4<T>(this IEnumerable<T> collection, IEnumerable<int> indices) {
              var rest = collection.GetEnumerator();
              foreach (int offset in indices.Distances()) {
                  Skip(rest, offset);
                  yield return rest.Current;
              }
          }
      
          static void Skip<T>(IEnumerator<T> enumerator, int skip) {
              while (skip > 0) {
                  enumerator.MoveNext();
                  skip--;
              }
              return;
          }
      
          static IEnumerable<int> Distances(this IEnumerable<int> numbers) {
              int offset = 0;
              foreach (var number in numbers) {
                  yield return number - offset;
                  offset = number;
              }
          }
      

      基准测试,为我们提供了与 Eric 解决方案相似的性能。

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Diagnostics;
      
      namespace ConsoleApplication21 {
      
          static class LinqExtensions {
      
              public static IEnumerable<T> GetIndexedItemsEric<T>(this IEnumerable<T> collection, IEnumerable<int> indices) {
                  int currentIndex = -1;
                  using (var collectionEnum = collection.GetEnumerator()) {
                      foreach (int index in indices) {
                          while (collectionEnum.MoveNext()) {
                              currentIndex += 1;
                              if (currentIndex == index) {
                                  yield return collectionEnum.Current;
                                  break;
                              }
                          }
                      }
                  }
              }
      
              public static IEnumerable<T> GetIndexedItemsSam<T>(this IEnumerable<T> collection, IEnumerable<int> indices) {
                  var rest = collection.GetEnumerator();
                  foreach (int offset in indices.Distances()) {
                      Skip(rest, offset);
                      yield return rest.Current;
                  }
              }
      
              static void Skip<T>(this IEnumerator<T> enumerator, int skip) {
                  while (skip > 0) {
                      enumerator.MoveNext();
                      skip--;
                  }
                  return;
              }
      
              static IEnumerable<int> Distances(this IEnumerable<int> numbers) {
                  int offset = 0;
                  foreach (var number in numbers) {
                      yield return number - offset;
                      offset = number;
                  }
              }
          } 
      
          class Program {
      
              static void TimeAction(string description, int iterations, Action func) {
                  var watch = new Stopwatch();
                  watch.Start();
                  for (int i = 0; i < iterations; i++) {
                      func(); 
                  }
                  watch.Stop();
                  Console.Write(description);
                  Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
              }
      
              static void Main(string[] args) {
      
                  int max = 100000;
                  int lookupCount = 1000;
                  int iterations = 500;
                  var rand = new Random();
                  var array = Enumerable.Range(0, max).ToArray();
                  var lookups = Enumerable.Range(0, lookupCount).Select(i => rand.Next(max - 1)).Distinct().OrderBy(_ => _).ToArray();
      
                  // warmup 
                  array.GetIndexedItemsEric(lookups).ToArray();
                  array.GetIndexedItemsSam(lookups).ToArray();
      
                  TimeAction("Eric's Solution", iterations, () => {
                      array.GetIndexedItemsEric(lookups).ToArray();
                  });
      
                  TimeAction("Sam's Solution", iterations, () =>
                  {
                      array.GetIndexedItemsEric(lookups).ToArray();
                  });
      
                  Console.ReadKey();
              }
          }
      }
      
      Eric 的求解时间经过 770 毫秒 Sam 的求解时间经过 768 毫秒

      【讨论】:

      • 不要忘记像这样进行分析时,代码第一次运行所花费的时间可能比随后的平均时间长得多,因为抖动只在第一次运行。当算法很快时,如果不考虑这一事实可能会严重扭曲结果,但无论出于何种原因,抖动在第一次时都有很多工作要做。如果您感兴趣的是每次运行的边际成本,而不是整体摊销成本,那么您可以考虑养成在秒表计时之外第一次调用“func”的习惯。
      • @Eric,真的,我应该让 TimeAction 在计时之前处理好抖动,我做了一个热身,以确保大多数东西都可以解决这个问题。我还在两个订单中对此进行了测试,以确保我的结果没有偏差,两种实现的性能几乎相同。
      【解决方案10】:

      我喜欢 linq。

          IList<T> list = collection.ToList<T>();
      
          var result = from i in indexes
                       select list[i];
      
          return result.ToList<T>();
      

      【讨论】:

        【解决方案11】:

        据我了解,ICollection 可能不一定有任何顺序,这就是为什么没有非常优雅的解决方案来按索引访问事物的原因。许多人想考虑使用字典或列表将数据存储在集合中。

        我能想到的最好方法是遍历集合,同时跟踪您所在的索引。然后检查索引列表是否包含该索引。如果是,则返回该元素。

        【讨论】:

        • 你自相矛盾。首先你说集合没有顺序,然后你说“遍历它”。如果它不可订购,则它不能被迭代。集合需要有顺序,因为它们需要实现 IEnumerable。在集合上的任何操作下,该顺序不需要是稳定的,但必须存在某种顺序。相关位不是排序,相关位是集合不需要有索引操作。
        • 您的算法草图不必要地低效,因为它没有利用索引列表已经处于正确顺序的事实。与其搜索列表,不如从列表中取出另一个迭代器,以跟踪下一个目标索引是什么。
        【解决方案12】:
            public static IEnumerable<T> WhereIndexes<T>(this IEnumerable<T> collection, IEnumerable<int> indexes)
            {
                IList<T> l = new List<T>(collection);
                foreach (var index in indexes)
                {
                    yield return l[index]; 
                }
            }
        

        【讨论】:

        • 这个算法有点误导。初读时我认为迭代索引而不是列表是有效的,但是复制列表会否定可能的好处。
        【解决方案13】:

        似乎最有效的方法是使用Dictionary&lt;int,T&gt; 而不是Collection&lt;T&gt;。您仍然可以在IList&lt;int&gt; 中保留要使用的索引列表。

        【讨论】:

          【解决方案14】:

          也许我遗漏了什么,但有什么问题:

          indexes.Select( (index => values[index]))
          

          【讨论】:

          • 集合没有 [ ] 运算符。
          • values 是一个 ICollection,所以它没有索引属性。
          猜你喜欢
          • 1970-01-01
          • 2011-02-12
          • 1970-01-01
          • 1970-01-01
          • 2019-11-22
          • 1970-01-01
          • 2020-12-15
          • 2010-12-15
          相关资源
          最近更新 更多