【问题标题】:LINQ to find series of consecutive numbersLINQ查找一系列连续数字
【发布时间】:2013-12-09 11:25:27
【问题描述】:

我有一个整数列表。我想在该列表中找到所有连续数字的运行,由起始索引和长度定义。例如,对于[1,2,3,5,7,8] 的输入列表,输出将是[{1,3}, {5,1}, {7,2}]。使用循环很容易做到这一点,就像这样(未经测试的伪代码):

for(i=1, i < maxNum; i++)
{
  number = list[i];
  previousNumber = list[i-1];
  if(number - previousNumber == 1)
  {
    runLength++;
  }
  else
  {
    result.Add(startingNumber, runLength);
    runLength = 1;
    startingNumber = number;
  }
}

但我认为可以使用 LINQ。任何想法如何做到这一点?

【问题讨论】:

  • 为什么必须在 Linq 中? (提示,跳过)
  • @L.B:有可能吗?它可能比上面的 for 循环代码更容易。当每个人都需要这样的东西时,它可能会为每个人提供另一种方式。
  • 因为我很好奇它是否可以使用可读的单线来完成,而 Linq 似乎是合适的工具。
  • 如果使用循环足够简单,请使用循环。有可能将一个解决方案硬塞到一个可读性可疑的 LINQ 查询中,但如果您有一个带有简单循环的完全可读的解决方案,为什么还要麻烦呢?
  • 在 5 种解决方案中进行选择?

标签: c# algorithm linq list


【解决方案1】:

一种 linqish 方式可以是编写扩展方法 GroupWhile 如下所示(省略所有检查。未经过优化以易于理解。)

int[] list = new int[] { 1, 2, 3, 5, 7, 8 };
var result = list.GroupWhile((x, y) => y - x == 1)
                 .Select(x => new {i = x.First(), len = x.Count()  })
                 .ToList();

public static IEnumerable<IEnumerable<T>> GroupWhile<T>(this IEnumerable<T> seq, Func<T,T,bool> condition)
{
    T prev = seq.First();
    List<T> list = new List<T>() { prev };

    foreach(T item in seq.Skip(1))
    {
        if(condition(prev,item)==false)
        {
            yield return list;
            list = new List<T>();
        }
        list.Add(item);
        prev = item;
    }

    yield return list;
}

TODO:使用IGrouping :)

【讨论】:

  • 1) 这是对源序列的多次迭代 2) 由于分组都是对同一个确切列表的引用,如果在生成下一个组之前没有处理每个组,则会导致严重问题.一个简单的GroupWhile(...).ToList() 将导致一堆对同一组的引用。您应该为每个组创建一个新列表。 3) 在foreach 的末尾,该组将始终至少有一项,从不为零,因此无需检查计数。
  • OMG 条件(prev,item)==false
【解决方案2】:

这似乎是一个合理的方法:

  1. 使用Range 压缩原始列表,因此每个元素都与其索引成元组
  2. 选择列表前任不是其自然前任的那些元素
  3. 转换为数组并保存到临时变量(方便最后一步)。
  4. 从索引中推导出子数组的长度。最后一项是与原始列表长度的差异。对于其他项目,它是与下一个索引的差异。

var list = new int[] { 1, 2, 3, 5, 7, 8 };
var filtered = list.Zip(Enumerable.Range(0, list.Length), Tuple.Create)
            .Where((x, i) => i == 0 || list[i - 1] != x.Item1 - 1).ToArray();

var result = filtered.Select((x, i) => i == filtered.Length - 1 
                ? Tuple.Create(x.Item1, list.Length - x.Item2) 
                : Tuple.Create(x.Item1, filtered[i + 1].Item2 - x.Item2));

foreach (var t in result)
{
    Console.WriteLine(t);
}

这会导致

(1, 3)
(5, 1)
(7, 2)

【讨论】:

    【解决方案3】:

    是否有人要求将解决方案硬塞到可读性可疑的 LINQ 查询中?

    var serieses = input.Aggregate(
        new List<Tuple<int, int>>(),
        (l, i) =>
            {
                var last = l.LastOrDefault();
                if (last == null ||
                    last.Item1 + last.Item2 != i)
                {
                    l.Add(new Tuple<int, int>(i, 1));
                }
                else if (last.Item1 + last.Item2 == i)
                {
                    l.RemoveAt(l.Count - 1);
                    l.Add(new Tuple<int, int>(last.Item1, last.Item2 + 1));
                }
    
                return l;
            },
        l => l);
    

    【讨论】:

      【解决方案4】:

      没有这种开箱即用的扩展方法,但您可以创建自己的:

      public static class LinqUtils{
          public class ConsecutiveGroup<T>
          {
              public T Left { get; internal set; }
              public T Right { get; internal set; }
              public long Count { get; internal set; }
          }
      
          public static IEnumerable<ConsecutiveGroup<T>> ConsecutiveCounts<T>(this IEnumerable<T> src, Func<T, T, bool> consecutive)
          {
              ConsecutiveGroup<T> current = null;
              foreach (var s in src)
              {
                  if (current==null)
                  {
                      current = new ConsecutiveGroup<T>
                          {
                              Left = s,
                              Right = s,
                              Count = 1
                          };
                      continue;
                  }
      
                  if(consecutive(current.Right, s))
                  {
                      current.Right = s;
                      current.Count += 1;
                      continue;
                  }
      
                  yield return current;
      
                  current = new ConsecutiveGroup<T>
                  {
                      Left = s,
                      Right = s,
                      Count = 1
                  };
              }
      
              if (current!=null)
              {
                  yield return current;
              }
          }
      }
      
      [TestFixture]
      public static class LinqUtilsTests
      {
          [Test]
          public void TestConsecutiveCounts()
          {
              var src = new[] {1,2,3,5,7,8};
      
              var expected = new[]
                  {
                      Tuple.Create<int,long>(1, 3),
                      Tuple.Create<int,long>(5, 1),
                      Tuple.Create<int,long>(7, 2)
                  };
      
              var result = src
                  .ConsecutiveCounts((prev, current) => current == (prev + 1))
                  .Select(c=>Tuple.Create(c.Left, c.Count));
      
              Assert.IsTrue(expected.SequenceEqual(result));
          }
      }
      

      【讨论】:

        【解决方案5】:

        试试这个:

        // Static class to contain the extension methods.
        public static class MyExtensions
        {
            public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
            {
                return source.ChunkBy(keySelector, EqualityComparer<TKey>.Default);
            }
        
            public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
            {
                // Flag to signal end of source sequence.
                const bool noMoreSourceElements = true;
        
                // Auto-generated iterator for the source array.       
                var enumerator = source.GetEnumerator();
        
                // Move to the first element in the source sequence.
                if (!enumerator.MoveNext()) yield break;
        
                // Iterate through source sequence and create a copy of each Chunk.
                // On each pass, the iterator advances to the first element of the next "Chunk"
                // in the source sequence. This loop corresponds to the outer foreach loop that
                // executes the query.
                Chunk<TKey, TSource> current = null;
                while (true)
                {
                    // Get the key for the current Chunk. The source iterator will churn through
                    // the source sequence until it finds an element with a key that doesn't match.
                    var key = keySelector(enumerator.Current);
        
                    // Make a new Chunk (group) object that initially has one GroupItem, which is a copy of the current source element.
                    current = new Chunk<TKey, TSource>(key, enumerator, value => comparer.Equals(key, keySelector(value)));
        
                    // Return the Chunk. A Chunk is an IGrouping<TKey,TSource>, which is the return value of the ChunkBy method.
                    // At this point the Chunk only has the first element in its source sequence. The remaining elements will be
                    // returned only when the client code foreach's over this chunk. See Chunk.GetEnumerator for more info.
                    yield return current;
        
                    // Check to see whether (a) the chunk has made a copy of all its source elements or 
                    // (b) the iterator has reached the end of the source sequence. If the caller uses an inner
                    // foreach loop to iterate the chunk items, and that loop ran to completion,
                    // then the Chunk.GetEnumerator method will already have made
                    // copies of all chunk items before we get here. If the Chunk.GetEnumerator loop did not
                    // enumerate all elements in the chunk, we need to do it here to avoid corrupting the iterator
                    // for clients that may be calling us on a separate thread.
                    if (current.CopyAllChunkElements() == noMoreSourceElements)
                    {
                        yield break;
                    }
                }
            }
        
            // A Chunk is a contiguous group of one or more source elements that have the same key. A Chunk 
            // has a key and a list of ChunkItem objects, which are copies of the elements in the source sequence.
            class Chunk<TKey, TSource> : IGrouping<TKey, TSource>
            {
                // INVARIANT: DoneCopyingChunk == true || 
                //   (predicate != null && predicate(enumerator.Current) && current.Value == enumerator.Current)
        
                // A Chunk has a linked list of ChunkItems, which represent the elements in the current chunk. Each ChunkItem
                // has a reference to the next ChunkItem in the list.
                class ChunkItem
                {
                    public ChunkItem(TSource value)
                    {
                        Value = value;
                    }
                    public readonly TSource Value;
                    public ChunkItem Next = null;
                }
        
                // The value that is used to determine matching elements
                private readonly TKey key;
        
                // Stores a reference to the enumerator for the source sequence
                private IEnumerator<TSource> enumerator;
        
                // A reference to the predicate that is used to compare keys.
                private Func<TSource, bool> predicate;
        
                // Stores the contents of the first source element that
                // belongs with this chunk.
                private readonly ChunkItem head;
        
                // End of the list. It is repositioned each time a new
                // ChunkItem is added.
                private ChunkItem tail;
        
                // Flag to indicate the source iterator has reached the end of the source sequence.
                internal bool isLastSourceElement = false;
        
                // Private object for thread syncronization
                private object m_Lock;
        
                public Chunk(TKey key, IEnumerator<TSource> enumerator, Func<TSource, bool> predicate)
                {
                    this.key = key;
                    this.enumerator = enumerator ?? throw new NullReferenceException(nameof(enumerator));
                    this.predicate = predicate ?? throw new NullReferenceException(nameof(predicate));
        
                    // A Chunk always contains at least one element.
                    head = new ChunkItem(enumerator.Current);
        
                    // The end and beginning are the same until the list contains > 1 elements.
                    tail = head;
        
                    m_Lock = new object();
                }
        
                // Indicates that all chunk elements have been copied to the list of ChunkItems, 
                // and the source enumerator is either at the end, or else on an element with a new key.
                // the tail of the linked list is set to null in the CopyNextChunkElement method if the
                // key of the next element does not match the current chunk's key, or there are no more elements in the source.
                private bool DoneCopyingChunk => tail == null;
        
                // Adds one ChunkItem to the current group
                // REQUIRES: !DoneCopyingChunk && lock(this)
                private void CopyNextChunkElement()
                {
                    // Try to advance the iterator on the source sequence.
                    // If MoveNext returns false we are at the end, and isLastSourceElement is set to true
                    isLastSourceElement = !enumerator.MoveNext();
        
                    // If we are (a) at the end of the source, or (b) at the end of the current chunk
                    // then null out the enumerator and predicate for reuse with the next chunk.
                    if (isLastSourceElement || !predicate(enumerator.Current))
                    {
                        enumerator = null;
                        predicate = null;
                    }
                    else
                    {
                        tail.Next = new ChunkItem(enumerator.Current);
                    }
        
                    // tail will be null if we are at the end of the chunk elements
                    // This check is made in DoneCopyingChunk.
                    tail = tail.Next;
                }
        
                // Called after the end of the last chunk was reached. It first checks whether
                // there are more elements in the source sequence. If there are, it 
                // Returns true if enumerator for this chunk was exhausted.
                internal bool CopyAllChunkElements()
                {
                    while (true)
                    {
                        lock (m_Lock)
                        {
                            if (DoneCopyingChunk)
                            {
                                // If isLastSourceElement is false,
                                // it signals to the outer iterator
                                // to continue iterating.
                                return isLastSourceElement;
                            }
                            else
                            {
                                CopyNextChunkElement();
                            }
                        }
                    }
                }
        
                public TKey Key => key;
        
                // Invoked by the inner foreach loop. This method stays just one step ahead
                // of the client requests. It adds the next element of the chunk only after
                // the clients requests the last element in the list so far.
                public IEnumerator<TSource> GetEnumerator()
                {
                    //Specify the initial element to enumerate.
                    ChunkItem current = head;
        
                    // There should always be at least one ChunkItem in a Chunk.
                    while (current != null)
                    {
                        // Yield the current item in the list.
                        yield return current.Value;
        
                        // Copy the next item from the source sequence, 
                        // if we are at the end of our local list.
                        lock (m_Lock)
                        {
                            if (current == tail)
                            {
                                CopyNextChunkElement();
                            }
                        }
        
                        // Move to the next ChunkItem in the list.
                        current = current.Next;
                    }
                }
        
                System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
            }
        }
        
        // A simple named type is used for easier viewing in the debugger. Anonymous types
        // work just as well with the ChunkBy operator.
        public class KeyValPair
        {
            public string Key { get; set; }
            public string Value { get; set; }
        }
        
        class Program
        {
            // The source sequence.
            public static IEnumerable<KeyValPair> list;
        
            // Query variable declared as class member to be available
            // on different threads.
            static IEnumerable<IGrouping<string, KeyValPair>> query;
        
            static void Main(string[] args)
            {
                // Initialize the source sequence with an array initializer.
                list = new[]
                {
                    new KeyValPair{ Key = "A", Value = "We" },
                    new KeyValPair{ Key = "A", Value = "think" },
                    new KeyValPair{ Key = "A", Value = "that" },
                    new KeyValPair{ Key = "B", Value = "Linq" },
                    new KeyValPair{ Key = "C", Value = "is" },
                    new KeyValPair{ Key = "A", Value = "really" },
                    new KeyValPair{ Key = "B", Value = "cool" },
                    new KeyValPair{ Key = "B", Value = "!" }
                };
        
                // Create the query by using our user-defined query operator.
                query = list.ChunkBy(p => p.Key);
        
                // ChunkBy returns IGrouping objects, therefore a nested
                // foreach loop is required to access the elements in each "chunk".
                foreach (var item in query)
                {
                    Console.WriteLine($"Group key = {item.Key}");
                    foreach (var inner in item)
                    {
                        Console.WriteLine($"\t{inner.Value}");
                    }
                }
        
                Console.WriteLine("Press any key to exit");
                Console.ReadKey();
            }
        }
        

        扩展方法实现了原生 IGrouping 接口(与 GroupBy 相同)。 存储是使用链接元素列表而不是列表完成的。 扩展方法的执行是线程安全和惰性的。

        【讨论】:

          【解决方案6】:

          您需要某种方式来扫描序列、累积结果然后对它们进行分组。所以首先是一个简单的Scan 扩展方法(类似于Aggregate,但它输出中间结果)(Scan 方法存在于IObservable 但不适用于IEnumerable):

          public static IEnumerable<U> Scan<T, U>(this IEnumerable<T> input,
                                                  Func<U, T, U> next,
                                                  U state)
          {
              yield return state;
              foreach (var item in input)
              {
                  state = next(state, item);
                  yield return state;
              }
          }
          

          并使用此方法和Zip 扩展方法,执行以下操作:

          var ints = new[] { 1, 2, 3, 5, 7, 8, 10 };
          var result = ints
              // Zip the list with itself shifted 1 to the left (add dummy value at the end)
              // and calculate the difference between each consecutive value.
              .Zip(ints
                  .Skip(1)
                  .Concat(new[] { int.MaxValue }), (i0, i1) => new { i = i0, diff = i1 - i0 })
              // Reverse because it's far easier keeping track of the group we're at
              .Reverse()
              // Scan through the list, assigning an incremental group number to each
              // consecutive sequence
              .Scan((state, z) => state == null
                  ? Tuple.Create(z.i, z.diff, 0)
                  : Tuple.Create(z.i, z.diff,
                      z.diff > 1 ? state.Item3 + 1 : state.Item3),
                  (Tuple<int, int, int>) null) // <-- dummy starting state.
              // Skip the dummy starting state we started the scan with
              .Skip(1)
              // Reverse back
              .Reverse()
              // Group by the group numbers we assigned during the scan
              .GroupBy(t => t.Item3, (i, l) => new { l.First().Item1, l = l.Count() });
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-09-04
            • 2023-03-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多