从给定列表中检测至少 3 个连续数字的序列

发布于 2024-09-25 23:30:54 字数 267 浏览 4 评论 0原文

我有一个数字列表,例如 21,4,7,9,12,22,17,8,2,20,23

我希望能够挑选出连续数字的序列(长度至少为 3 个项目),所以从上面的例子是 7,8,9 和 20,21,22,23。

我尝试过一些丑陋的庞大函数,但我想知道是否有一种简洁的 LINQ 风格的方法来做到这一点。

有什么建议吗?

更新:

非常感谢您的所有回复,非常感谢。我目前正在与他们一起玩,看看哪一个最适合融入我们的项目。

I have a list of numbers e.g. 21,4,7,9,12,22,17,8,2,20,23

I want to be able to pick out sequences of sequential numbers (minimum 3 items in length), so from the example above it would be 7,8,9 and 20,21,22,23.

I have played around with a few ugly sprawling functions but I am wondering if there is a neat LINQ-ish way to do it.

Any suggestions?

UPDATE:

Many thanks for all the responses, much appriciated. Im am currently having a play with them all to see which would best integrate into our project.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

小情绪 2024-10-02 23:30:54

我突然意识到你应该做的第一件事就是订购清单。然后只需遍历它,记住当前序列的长度并检测它何时结束即可。老实说,我怀疑简单的 foreach 循环将是最简单的方法 - 我无法立即想到任何非常简洁的类似 LINQ 的方法。如果您确实愿意,您当然可以在迭代器块中执行此操作,但请记住,对列表进行排序意味着无论如何您都有相当的“前期”成本。所以我的解决方案看起来像这样:

var ordered = list.OrderBy(x => x);
int count = 0;
int firstItem = 0; // Irrelevant to start with
foreach (int x in ordered)
{
    // First value in the ordered list: start of a sequence
    if (count == 0)
    {
        firstItem = x;
        count = 1;
    }
    // Skip duplicate values
    else if (x == firstItem + count - 1)
    {
        // No need to do anything
    }
    // New value contributes to sequence
    else if (x == firstItem + count)
    {
        count++;
    }
    // End of one sequence, start of another
    else
    {
        if (count >= 3)
        {
            Console.WriteLine("Found sequence of length {0} starting at {1}",
                              count, firstItem);
        }
        count = 1;
        firstItem = x;
    }
}
if (count >= 3)
{
    Console.WriteLine("Found sequence of length {0} starting at {1}",
                      count, firstItem);
}

编辑:好的,我只是想到了一种更类似于 LINQ 的做事方式。我现在没有时间完全实现它,但是:

  • 排序序列
  • 使用类似 SelectWithPrevious(可能更好地命名为SelectConsecutive)来获取连续的元素
  • 对使用包含索引的 Select 重载来获取 (index,当前、上一个)
  • 过滤掉 (当前 = 上一个 + 1) 处的任何项目,以获取算作序列开始的任何位置(特殊情况索引=0)
  • 使用 SelectWithPrevious 在结果上获取两个起点之间序列的长度(从前一个索引中减去一个索引)
  • 过滤掉任何长度小于3的序列

怀疑你需要在有序序列上连接int.MinValue,以保证最终项目被正确使用。

编辑:好的,我已经实现了这个。这是我能想到的最 LINQest 方法...我使用 null 值作为“哨兵”值来强制开始和结束序列 - 有关更多详细信息,请参阅注释。

总的来说,我不会推荐这个解决方案。很难让你清醒过来,尽管我相当有信心它是正确的,但我花了一段时间思考可能存在的相差一错误等。这是一次有趣的探索你可以做什么的旅程使用 LINQ...以及您可能不应该做的事情。

哦,请注意,我已将“最小长度 3”部分推送给调用者 - 当您有这样的元组序列时,单独过滤掉它会更干净,IMO。

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

static class Extensions
{
    public static IEnumerable<TResult> SelectConsecutive<TSource, TResult>
        (this IEnumerable<TSource> source,
         Func<TSource, TSource, TResult> selector)
    {
        using (IEnumerator<TSource> iterator = source.GetEnumerator())
        {
           if (!iterator.MoveNext())
           {
               yield break;
           }
           TSource prev = iterator.Current;
           while (iterator.MoveNext())
           {
               TSource current = iterator.Current;
               yield return selector(prev, current);
               prev = current;
           }
        }
    }
}

class Test
{
    static void Main()
    {
        var list = new List<int> {  21,4,7,9,12,22,17,8,2,20,23 };

        foreach (var sequence in FindSequences(list).Where(x => x.Item1 >= 3))
        {
            Console.WriteLine("Found sequence of length {0} starting at {1}",
                              sequence.Item1, sequence.Item2);
        }
    }

    private static readonly int?[] End = { null };

    // Each tuple in the returned sequence is (length, first element)
    public static IEnumerable<Tuple<int, int>> FindSequences
         (IEnumerable<int> input)
    {
        // Use null values at the start and end of the ordered sequence
        // so that the first pair always starts a new sequence starting
        // with the lowest actual element, and the final pair always
        // starts a new one starting with null. That "sequence at the end"
        // is used to compute the length of the *real* final element.
        return End.Concat(input.OrderBy(x => x)
                               .Select(x => (int?) x))
                  .Concat(End)
                  // Work out consecutive pairs of items
                  .SelectConsecutive((x, y) => Tuple.Create(x, y))
                  // Remove duplicates
                  .Where(z => z.Item1 != z.Item2)
                  // Keep the index so we can tell sequence length
                  .Select((z, index) => new { z, index })
                  // Find sequence starting points
                  .Where(both => both.z.Item2 != both.z.Item1 + 1)
                  .SelectConsecutive((start1, start2) => 
                       Tuple.Create(start2.index - start1.index, 
                                    start1.z.Item2.Value));
    }
}

It strikes me that the first thing you should do is order the list. Then it's just a matter of walking through it, remembering the length of your current sequence and detecting when it's ended. To be honest, I suspect that a simple foreach loop is going to be the simplest way of doing that - I can't immediately think of any wonderfully neat LINQ-like ways of doing it. You could certainly do it in an iterator block if you really wanted to, but bear in mind that ordering the list to start with means you've got a reasonably "up-front" cost anyway. So my solution would look something like this:

var ordered = list.OrderBy(x => x);
int count = 0;
int firstItem = 0; // Irrelevant to start with
foreach (int x in ordered)
{
    // First value in the ordered list: start of a sequence
    if (count == 0)
    {
        firstItem = x;
        count = 1;
    }
    // Skip duplicate values
    else if (x == firstItem + count - 1)
    {
        // No need to do anything
    }
    // New value contributes to sequence
    else if (x == firstItem + count)
    {
        count++;
    }
    // End of one sequence, start of another
    else
    {
        if (count >= 3)
        {
            Console.WriteLine("Found sequence of length {0} starting at {1}",
                              count, firstItem);
        }
        count = 1;
        firstItem = x;
    }
}
if (count >= 3)
{
    Console.WriteLine("Found sequence of length {0} starting at {1}",
                      count, firstItem);
}

EDIT: Okay, I've just thought of a rather more LINQ-ish way of doing things. I don't have the time to fully implement it now, but:

  • Order the sequence
  • Use something like SelectWithPrevious (probably better named SelectConsecutive) to get consecutive pairs of elements
  • Use the overload of Select which includes the index to get tuples of (index, current, previous)
  • Filter out any items where (current = previous + 1) to get anywhere that counts as the start of a sequence (special-case index=0)
  • Use SelectWithPrevious on the result to get the length of the sequence between two starting points (subtract one index from the previous)
  • Filter out any sequence with length less than 3

I suspect you need to concat int.MinValue on the ordered sequence, to guarantee the final item is used properly.

EDIT: Okay, I've implemented this. It's about the LINQiest way I can think of to do this... I used null values as "sentinel" values to force start and end sequences - see comments for more details.

Overall, I wouldn't recommend this solution. It's hard to get your head round, and although I'm reasonably confident it's correct, it took me a while thinking of possible off-by-one errors etc. It's an interesting voyage into what you can do with LINQ... and also what you probably shouldn't.

Oh, and note that I've pushed the "minimum length of 3" part up to the caller - when you have a sequence of tuples like this, it's cleaner to filter it out separately, IMO.

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

static class Extensions
{
    public static IEnumerable<TResult> SelectConsecutive<TSource, TResult>
        (this IEnumerable<TSource> source,
         Func<TSource, TSource, TResult> selector)
    {
        using (IEnumerator<TSource> iterator = source.GetEnumerator())
        {
           if (!iterator.MoveNext())
           {
               yield break;
           }
           TSource prev = iterator.Current;
           while (iterator.MoveNext())
           {
               TSource current = iterator.Current;
               yield return selector(prev, current);
               prev = current;
           }
        }
    }
}

class Test
{
    static void Main()
    {
        var list = new List<int> {  21,4,7,9,12,22,17,8,2,20,23 };

        foreach (var sequence in FindSequences(list).Where(x => x.Item1 >= 3))
        {
            Console.WriteLine("Found sequence of length {0} starting at {1}",
                              sequence.Item1, sequence.Item2);
        }
    }

    private static readonly int?[] End = { null };

    // Each tuple in the returned sequence is (length, first element)
    public static IEnumerable<Tuple<int, int>> FindSequences
         (IEnumerable<int> input)
    {
        // Use null values at the start and end of the ordered sequence
        // so that the first pair always starts a new sequence starting
        // with the lowest actual element, and the final pair always
        // starts a new one starting with null. That "sequence at the end"
        // is used to compute the length of the *real* final element.
        return End.Concat(input.OrderBy(x => x)
                               .Select(x => (int?) x))
                  .Concat(End)
                  // Work out consecutive pairs of items
                  .SelectConsecutive((x, y) => Tuple.Create(x, y))
                  // Remove duplicates
                  .Where(z => z.Item1 != z.Item2)
                  // Keep the index so we can tell sequence length
                  .Select((z, index) => new { z, index })
                  // Find sequence starting points
                  .Where(both => both.z.Item2 != both.z.Item1 + 1)
                  .SelectConsecutive((start1, start2) => 
                       Tuple.Create(start2.index - start1.index, 
                                    start1.z.Item2.Value));
    }
}
凉栀 2024-10-02 23:30:54

Jon Skeet / Timwi 的解决方案是正确的选择。

为了好玩,这里有一个 LINQ 查询来完成这项工作(非常效率低下):

var sequences = input.Distinct()
                     .GroupBy(num => Enumerable.Range(num, int.MaxValue - num + 1)
                                               .TakeWhile(input.Contains)
                                               .Last())  //use the last member of the consecutive sequence as the key
                     .Where(seq => seq.Count() >= 3)
                     .Select(seq => seq.OrderBy(num => num)); // not necessary unless ordering is desirable inside each sequence.

通过将输入加载到 HashSet 中,可以稍微提高查询的性能(以提高 包含),但这仍然不会产生一个接近高效的解决方案。

我知道的唯一错误是,如果序列包含大量的负数,则可能会出现算术溢出(我们无法表示Range<的count参数) /代码>)。使用自定义 static IEnumerable可以很容易地解决这个问题。 To(this int start, int end) 扩展方法。如果有人能想到任何其他简单的避免溢出的技术,请告诉我。

编辑:
这是一个稍微详细(但同样低效)的变体,没有溢出问题。

var sequences = input.GroupBy(num => input.Where(candidate => candidate >= num)
                                          .OrderBy(candidate => candidate)
                                          .TakeWhile((candidate, index) => candidate == num + index)
                                          .Last())
                     .Where(seq => seq.Count() >= 3)
                     .Select(seq => seq.OrderBy(num => num));

Jon Skeet's / Timwi's solutions are the way to go.

For fun, here's a LINQ query that does the job (very inefficiently):

var sequences = input.Distinct()
                     .GroupBy(num => Enumerable.Range(num, int.MaxValue - num + 1)
                                               .TakeWhile(input.Contains)
                                               .Last())  //use the last member of the consecutive sequence as the key
                     .Where(seq => seq.Count() >= 3)
                     .Select(seq => seq.OrderBy(num => num)); // not necessary unless ordering is desirable inside each sequence.

The query's performance can be improved slightly by loading the input into a HashSet (to improve Contains), but that will still not produce a solution that is anywhere close to efficient.

The only bug I am aware of is the possibility of an arithmetic overflow if the sequence contains negative numbers of large magnitude (we cannot represent the count parameter for Range). This would be easy to fix with a custom static IEnumerable<int> To(this int start, int end) extension-method. If anyone can think of any other simple technique of dodging the overflow, please let me know.

EDIT:
Here's a slightly more verbose (but equally inefficient) variant without the overflow issue.

var sequences = input.GroupBy(num => input.Where(candidate => candidate >= num)
                                          .OrderBy(candidate => candidate)
                                          .TakeWhile((candidate, index) => candidate == num + index)
                                          .Last())
                     .Where(seq => seq.Count() >= 3)
                     .Select(seq => seq.OrderBy(num => num));
つ可否回来 2024-10-02 23:30:54

我认为我的解决方案更加优雅和简单,因此更容易验证其正确性:

/// <summary>Returns a collection containing all consecutive sequences of
/// integers in the input collection.</summary>
/// <param name="input">The collection of integers in which to find
/// consecutive sequences.</param>
/// <param name="minLength">Minimum length that a sequence should have
/// to be returned.</param>
static IEnumerable<IEnumerable<int>> ConsecutiveSequences(
    IEnumerable<int> input, int minLength = 1)
{
    var results = new List<List<int>>();
    foreach (var i in input.OrderBy(x => x))
    {
        var existing = results.FirstOrDefault(lst => lst.Last() + 1 == i);
        if (existing == null)
            results.Add(new List<int> { i });
        else
            existing.Add(i);
    }
    return minLength <= 1 ? results :
        results.Where(lst => lst.Count >= minLength);
}

相对于其他解决方案的优点:

  • 它可以找到重叠的序列。
  • 它可以正确地重用并记录下来。
  • 我还没有发现任何错误;-)

I think my solution is more elegant and simple, and therefore easier to verify as correct:

/// <summary>Returns a collection containing all consecutive sequences of
/// integers in the input collection.</summary>
/// <param name="input">The collection of integers in which to find
/// consecutive sequences.</param>
/// <param name="minLength">Minimum length that a sequence should have
/// to be returned.</param>
static IEnumerable<IEnumerable<int>> ConsecutiveSequences(
    IEnumerable<int> input, int minLength = 1)
{
    var results = new List<List<int>>();
    foreach (var i in input.OrderBy(x => x))
    {
        var existing = results.FirstOrDefault(lst => lst.Last() + 1 == i);
        if (existing == null)
            results.Add(new List<int> { i });
        else
            existing.Add(i);
    }
    return minLength <= 1 ? results :
        results.Where(lst => lst.Count >= minLength);
}

Benefits over the other solutions:

  • It can find sequences that overlap.
  • It’s properly reusable and documented.
  • I have not found any bugs ;-)
夏有森光若流苏 2024-10-02 23:30:54

以下是如何以“LINQish”方式解决问题:

int[] arr = new int[]{ 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };
IOrderedEnumerable<int> sorted = arr.OrderBy(x => x);
int cnt = sorted.Count();
int[] sortedArr = sorted.ToArray();
IEnumerable<int> selected = sortedArr.Where((x, idx) =>
    idx <= cnt - 3 && sortedArr[idx + 1] == x + 1 && sortedArr[idx + 2] == x + 2);
IEnumerable<int> result = selected.SelectMany(x => new int[] { x, x + 1, x + 2 }).Distinct();

Console.WriteLine(string.Join(",", result.Select(x=>x.ToString()).ToArray()));

由于数组复制和重建,该解决方案当然不如传统的循环解决方案高效。

Here is how to solve the problem in a "LINQish" way:

int[] arr = new int[]{ 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };
IOrderedEnumerable<int> sorted = arr.OrderBy(x => x);
int cnt = sorted.Count();
int[] sortedArr = sorted.ToArray();
IEnumerable<int> selected = sortedArr.Where((x, idx) =>
    idx <= cnt - 3 && sortedArr[idx + 1] == x + 1 && sortedArr[idx + 2] == x + 2);
IEnumerable<int> result = selected.SelectMany(x => new int[] { x, x + 1, x + 2 }).Distinct();

Console.WriteLine(string.Join(",", result.Select(x=>x.ToString()).ToArray()));

Due to the array copying and reconstruction, this solution - of course - is not as efficient as the traditional solution with loops.

潇烟暮雨 2024-10-02 23:30:54

不是 100% Linq,但这是一个通用变体:

static IEnumerable<IEnumerable<TItem>> GetSequences<TItem>(
        int minSequenceLength, 
        Func<TItem, TItem, bool> areSequential, 
        IEnumerable<TItem> items)
    where TItem : IComparable<TItem>
{
    items = items
        .OrderBy(n => n)
        .Distinct().ToArray();

    var lastSelected = default(TItem);

    var sequences =
        from startItem in items
        where startItem.Equals(items.First())
            || startItem.CompareTo(lastSelected) > 0
        let sequence =
            from item in items
            where item.Equals(startItem) || areSequential(lastSelected, item)
            select (lastSelected = item)
        where sequence.Count() >= minSequenceLength
        select sequence;

    return sequences;
}

static void UsageInt()
{
    var sequences = GetSequences(
            3,
            (a, b) => a + 1 == b,
            new[] { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 });

    foreach (var sequence in sequences)
        Console.WriteLine(string.Join(", ", sequence.ToArray()));
}

static void UsageChar()
{
    var list = new List<char>(
        "abcdefghijklmnopqrstuvwxyz".ToCharArray());

    var sequences = GetSequences(
            3,
            (a, b) => (list.IndexOf(a) + 1 == list.IndexOf(b)),
            "PleaseBeGentleWithMe".ToLower().ToCharArray());

    foreach (var sequence in sequences)
        Console.WriteLine(string.Join(", ", sequence.ToArray()));
}

Not 100% Linq but here's a generic variant:

static IEnumerable<IEnumerable<TItem>> GetSequences<TItem>(
        int minSequenceLength, 
        Func<TItem, TItem, bool> areSequential, 
        IEnumerable<TItem> items)
    where TItem : IComparable<TItem>
{
    items = items
        .OrderBy(n => n)
        .Distinct().ToArray();

    var lastSelected = default(TItem);

    var sequences =
        from startItem in items
        where startItem.Equals(items.First())
            || startItem.CompareTo(lastSelected) > 0
        let sequence =
            from item in items
            where item.Equals(startItem) || areSequential(lastSelected, item)
            select (lastSelected = item)
        where sequence.Count() >= minSequenceLength
        select sequence;

    return sequences;
}

static void UsageInt()
{
    var sequences = GetSequences(
            3,
            (a, b) => a + 1 == b,
            new[] { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 });

    foreach (var sequence in sequences)
        Console.WriteLine(string.Join(", ", sequence.ToArray()));
}

static void UsageChar()
{
    var list = new List<char>(
        "abcdefghijklmnopqrstuvwxyz".ToCharArray());

    var sequences = GetSequences(
            3,
            (a, b) => (list.IndexOf(a) + 1 == list.IndexOf(b)),
            "PleaseBeGentleWithMe".ToLower().ToCharArray());

    foreach (var sequence in sequences)
        Console.WriteLine(string.Join(", ", sequence.ToArray()));
}
离线来电— 2024-10-02 23:30:54

这是我的尝试:

public static class SequenceDetector
{
    public static IEnumerable<IEnumerable<T>> DetectSequenceWhere<T>(this IEnumerable<T> sequence, Func<T, T, bool> inSequenceSelector)
    {
        List<T> subsequence = null;
        // We can only have a sequence with 2 or more items
        T last = sequence.FirstOrDefault();
        foreach (var item in sequence.Skip(1))
        {
            if (inSequenceSelector(last, item))
            {
                // These form part of a sequence
                if (subsequence == null)
                {
                    subsequence = new List<T>();
                    subsequence.Add(last);
                }
                subsequence.Add(item);
            }
            else if (subsequence != null)
            {
                // We have a previous seq to return
                yield return subsequence;
                subsequence = null;
            }
            last = item;
        }
        if (subsequence != null)
        {
            // Return any trailing seq
            yield return subsequence;
        }
    }
}

public class test
{
    public static void run()
    {
        var list = new List<int> { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };
        foreach (var subsequence in list
            .OrderBy(i => i)
            .Distinct()
            .DetectSequenceWhere((first, second) => first + 1 == second)
            .Where(seq => seq.Count() >= 3))
        {
            Console.WriteLine("Found subsequence {0}", 
                string.Join(", ", subsequence.Select(i => i.ToString()).ToArray()));
        }
    }
}

这将返回形成子序列的特定项目,并允许任何类型的项目和任何标准定义,只要它可以通过比较相邻项目来确定即可。

Here's my shot at it:

public static class SequenceDetector
{
    public static IEnumerable<IEnumerable<T>> DetectSequenceWhere<T>(this IEnumerable<T> sequence, Func<T, T, bool> inSequenceSelector)
    {
        List<T> subsequence = null;
        // We can only have a sequence with 2 or more items
        T last = sequence.FirstOrDefault();
        foreach (var item in sequence.Skip(1))
        {
            if (inSequenceSelector(last, item))
            {
                // These form part of a sequence
                if (subsequence == null)
                {
                    subsequence = new List<T>();
                    subsequence.Add(last);
                }
                subsequence.Add(item);
            }
            else if (subsequence != null)
            {
                // We have a previous seq to return
                yield return subsequence;
                subsequence = null;
            }
            last = item;
        }
        if (subsequence != null)
        {
            // Return any trailing seq
            yield return subsequence;
        }
    }
}

public class test
{
    public static void run()
    {
        var list = new List<int> { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };
        foreach (var subsequence in list
            .OrderBy(i => i)
            .Distinct()
            .DetectSequenceWhere((first, second) => first + 1 == second)
            .Where(seq => seq.Count() >= 3))
        {
            Console.WriteLine("Found subsequence {0}", 
                string.Join(", ", subsequence.Select(i => i.ToString()).ToArray()));
        }
    }
}

This returns the specific items that form the sub-sequences and permits any type of item and any definition of criteria so long as it can be determined by comparing adjacent items.

无悔心 2024-10-02 23:30:54

对数组进行排序,然后创建另一个数组,该数组是每个元素与前一个元素之间的差异


sortedArray = 8, 9, 10, 21, 22, 23, 24, 27, 30, 31, 32
diffArray   =    1,  1, 11,  1,  1,  1,  3,  3,  1,  1

Now iterate through the difference array; if the difference equlas 1, increase the count of a variable, sequenceLength, by 1. If the difference is > 1, check the sequenceLength if it is >=2 then you have a sequence of at at least 3 consecutive elements. Then reset sequenceLenght to 0 and continue your loop on the difference array.

What about sorting the array then create another array that is the difference between each element the previous one


sortedArray = 8, 9, 10, 21, 22, 23, 24, 27, 30, 31, 32
diffArray   =    1,  1, 11,  1,  1,  1,  3,  3,  1,  1

Now iterate through the difference array; if the difference equlas 1, increase the count of a variable, sequenceLength, by 1. If the difference is > 1, check the sequenceLength if it is >=2 then you have a sequence of at at least 3 consecutive elements. Then reset sequenceLenght to 0 and continue your loop on the difference array.

往日 2024-10-02 23:30:54

这是我在 F# 中提出的一个解决方案,将其转换为 C# LINQ 查询应该相当容易,因为 Fold 几乎等同于 LINQ 聚合运算符。

#light

let nums = [21;4;7;9;12;22;17;8;2;20;23]

let scanFunc (mainSeqLength, mainCounter, lastNum:int, subSequenceCounter:int, subSequence:'a list, foundSequences:'a list list) (num:'a) =
        (mainSeqLength, mainCounter + 1,
         num,
         (if num <> lastNum + 1 then 1 else subSequenceCounter+1),
         (if num <> lastNum + 1 then [num] else subSequence@[num]),
         if subSequenceCounter >= 3 then
            if mainSeqLength = mainCounter+1
                then foundSequences @ [subSequence@[num]]
            elif num <> lastNum + 1
                then foundSequences @ [subSequence]
            else foundSequences
         else foundSequences)

let subSequences = nums |> Seq.sort |> Seq.fold scanFunc (nums |> Seq.length, 0, 0, 0, [], []) |> fun (_,_,_,_,_,results) -> results

Here is a solution I knocked up in F#, it should be fairly easy to translate this into a C# LINQ query since fold is pretty much equivalent to the LINQ aggregate operator.

#light

let nums = [21;4;7;9;12;22;17;8;2;20;23]

let scanFunc (mainSeqLength, mainCounter, lastNum:int, subSequenceCounter:int, subSequence:'a list, foundSequences:'a list list) (num:'a) =
        (mainSeqLength, mainCounter + 1,
         num,
         (if num <> lastNum + 1 then 1 else subSequenceCounter+1),
         (if num <> lastNum + 1 then [num] else subSequence@[num]),
         if subSequenceCounter >= 3 then
            if mainSeqLength = mainCounter+1
                then foundSequences @ [subSequence@[num]]
            elif num <> lastNum + 1
                then foundSequences @ [subSequence]
            else foundSequences
         else foundSequences)

let subSequences = nums |> Seq.sort |> Seq.fold scanFunc (nums |> Seq.length, 0, 0, 0, [], []) |> fun (_,_,_,_,_,results) -> results
始终不够爱げ你 2024-10-02 23:30:54

Linq 并不是万能的解决方案,有时您最好使用简单的循环。这是一个解决方案,只需一点 Linq 即可对原始序列进行排序并过滤结果

void Main()
{
    var numbers = new[] { 21,4,7,9,12,22,17,8,2,20,23 };
    var sequences =
        GetSequences(numbers, (prev, curr) => curr == prev + 1);
        .Where(s => s.Count() >= 3);
    sequences.Dump();
}

public static IEnumerable<IEnumerable<T>> GetSequences<T>(
    IEnumerable<T> source,
    Func<T, T, bool> areConsecutive)
{
    bool first = true;
    T prev = default(T);
    List<T> seq = new List<T>();
    foreach (var i in source.OrderBy(i => i))
    {
        if (!first && !areConsecutive(prev, i))
        {
            yield return seq.ToArray();
            seq.Clear();
        }
        first = false;
        seq.Add(i);
        prev = i;
    }
    if (seq.Any())
        yield return seq.ToArray();
}

Linq isn't the solution for everything, sometimes you're better of with a simple loop. Here's a solution, with just a bit of Linq to order the original sequences and filter the results

void Main()
{
    var numbers = new[] { 21,4,7,9,12,22,17,8,2,20,23 };
    var sequences =
        GetSequences(numbers, (prev, curr) => curr == prev + 1);
        .Where(s => s.Count() >= 3);
    sequences.Dump();
}

public static IEnumerable<IEnumerable<T>> GetSequences<T>(
    IEnumerable<T> source,
    Func<T, T, bool> areConsecutive)
{
    bool first = true;
    T prev = default(T);
    List<T> seq = new List<T>();
    foreach (var i in source.OrderBy(i => i))
    {
        if (!first && !areConsecutive(prev, i))
        {
            yield return seq.ToArray();
            seq.Clear();
        }
        first = false;
        seq.Add(i);
        prev = i;
    }
    if (seq.Any())
        yield return seq.ToArray();
}
霓裳挽歌倾城醉 2024-10-02 23:30:54

我想到了同样的事情 Jon:要表示一系列连续整数,您真正需要的只是两个微不足道的整数!所以我将从这里开始:

struct Range : IEnumerable<int>
{
    readonly int _start;
    readonly int _count;

    public Range(int start, int count)
    {
        _start = start;
        _count = count;
    }

    public int Start
    {
        get { return _start; }
    }

    public int Count
    {
        get { return _count; }
    }

    public int End
    {
        get { return _start + _count - 1; }
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i < _count; ++i)
        {
            yield return _start + i;
        }
    }

    // Heck, why not?
    public static Range operator +(Range x, int y)
    {
        return new Range(x.Start, x.Count + y);
    }

    // skipping the explicit IEnumerable.GetEnumerator implementation
}

从那里,您可以编写一个静态方法来返回一堆与序列的连续数字相对应的 Range 值。

static IEnumerable<Range> FindRanges(IEnumerable<int> source, int minCount)
{
    // throw exceptions on invalid arguments, maybe...

    var ordered = source.OrderBy(x => x);

    Range r = default(Range);

    foreach (int value in ordered)
    {
        // In "real" code I would've overridden the Equals method
        // and overloaded the == operator to write something like
        // if (r == Range.Empty) here... but this works well enough
        // for now, since the only time r.Count will be 0 is on the
        // first item.
        if (r.Count == 0)
        {
            r = new Range(value, 1);
            continue;
        }

        if (value == r.End)
        {
            // skip duplicates
            continue;
        }
        else if (value == r.End + 1)
        {
            // "append" consecutive values to the range
            r += 1;
        }
        else
        {
            // return what we've got so far
            if (r.Count >= minCount)
            {
                yield return r;
            }

            // start over
            r = new Range(value, 1);
        }
    }

    // return whatever we ended up with
    if (r.Count >= minCount)
    {
        yield return r;
    }
}

演示:

int[] numbers = new[] { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };

foreach (Range r in FindConsecutiveRanges(numbers, 3))
{
    // Using .NET 3.5 here, don't have the much nicer string.Join overloads.
    Console.WriteLine(string.Join(", ", r.Select(x => x.ToString()).ToArray()));
}

输出:

7, 8, 9
20, 21, 22, 23

I thought of the same thing as Jon: to represent a range of consecutive integers all you really need are two measly integers! So I'd start there:

struct Range : IEnumerable<int>
{
    readonly int _start;
    readonly int _count;

    public Range(int start, int count)
    {
        _start = start;
        _count = count;
    }

    public int Start
    {
        get { return _start; }
    }

    public int Count
    {
        get { return _count; }
    }

    public int End
    {
        get { return _start + _count - 1; }
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i < _count; ++i)
        {
            yield return _start + i;
        }
    }

    // Heck, why not?
    public static Range operator +(Range x, int y)
    {
        return new Range(x.Start, x.Count + y);
    }

    // skipping the explicit IEnumerable.GetEnumerator implementation
}

From there, you can write a static method to return a bunch of these Range values corresponding to the consecutive numbers of your sequence.

static IEnumerable<Range> FindRanges(IEnumerable<int> source, int minCount)
{
    // throw exceptions on invalid arguments, maybe...

    var ordered = source.OrderBy(x => x);

    Range r = default(Range);

    foreach (int value in ordered)
    {
        // In "real" code I would've overridden the Equals method
        // and overloaded the == operator to write something like
        // if (r == Range.Empty) here... but this works well enough
        // for now, since the only time r.Count will be 0 is on the
        // first item.
        if (r.Count == 0)
        {
            r = new Range(value, 1);
            continue;
        }

        if (value == r.End)
        {
            // skip duplicates
            continue;
        }
        else if (value == r.End + 1)
        {
            // "append" consecutive values to the range
            r += 1;
        }
        else
        {
            // return what we've got so far
            if (r.Count >= minCount)
            {
                yield return r;
            }

            // start over
            r = new Range(value, 1);
        }
    }

    // return whatever we ended up with
    if (r.Count >= minCount)
    {
        yield return r;
    }
}

Demo:

int[] numbers = new[] { 21, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };

foreach (Range r in FindConsecutiveRanges(numbers, 3))
{
    // Using .NET 3.5 here, don't have the much nicer string.Join overloads.
    Console.WriteLine(string.Join(", ", r.Select(x => x.ToString()).ToArray()));
}

Output:

7, 8, 9
20, 21, 22, 23
顾忌 2024-10-02 23:30:54

这是我对这个问题的 LINQ-y 看法:

static IEnumerable<IEnumerable<int>>
    ConsecutiveSequences(this IEnumerable<int> input, int minLength = 3)
{
    int order = 0;
    var inorder = new SortedSet<int>(input);
    return from item in new[] { new { order = 0, val = inorder.First() } }
               .Concat(
                 inorder.Zip(inorder.Skip(1), (x, val) =>
                         new { order = x + 1 == val ? order : ++order, val }))
           group item.val by item.order into list
           where list.Count() >= minLength
           select list;
}
  • 不使用显式循环,但仍然应该是 O(n lg n)
  • 使用 SortedSet 而不是 .OrderBy().Distinct()
  • 将连续元素与 list.Zip(list.Skip(1)) 结合起来

Here's my LINQ-y take on the problem:

static IEnumerable<IEnumerable<int>>
    ConsecutiveSequences(this IEnumerable<int> input, int minLength = 3)
{
    int order = 0;
    var inorder = new SortedSet<int>(input);
    return from item in new[] { new { order = 0, val = inorder.First() } }
               .Concat(
                 inorder.Zip(inorder.Skip(1), (x, val) =>
                         new { order = x + 1 == val ? order : ++order, val }))
           group item.val by item.order into list
           where list.Count() >= minLength
           select list;
}
  • uses no explicit loops, but should still be O(n lg n)
  • uses SortedSet instead of .OrderBy().Distinct()
  • combines consecutive element with list.Zip(list.Skip(1))
谈场末日恋爱 2024-10-02 23:30:54

这是使用字典而不是排序的解决方案......
它将项目添加到字典中,然后对于每个值在上方和下方递增以找到最长的序列。
它不是严格意义上的 LINQ,尽管它确实使用了一些 LINQ 函数,而且我认为它比纯 LINQ 解决方案更具可读性。

static void Main(string[] args)
    {
        var items = new[] { -1, 0, 1, 21, -2, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };
        IEnumerable<IEnumerable<int>> sequences = FindSequences(items, 3);

        foreach (var sequence in sequences)
        {   //print results to consol
            Console.Out.WriteLine(sequence.Select(num => num.ToString()).Aggregate((a, b) => a + "," + b));
        }
        Console.ReadLine();
    }

    private static IEnumerable<IEnumerable<int>> FindSequences(IEnumerable<int> items, int minSequenceLength)
    {
        //Convert item list to dictionary
        var itemDict = new Dictionary<int, int>();
        foreach (int val in items)
        {
            itemDict[val] = val;
        }
        var allSequences = new List<List<int>>();
        //for each val in items, find longest sequence including that value
        foreach (var item in items)
        {
            var sequence = FindLongestSequenceIncludingValue(itemDict, item);
            allSequences.Add(sequence);
            //remove items from dict to prevent duplicate sequences
            sequence.ForEach(i => itemDict.Remove(i));
        }
        //return only sequences longer than 3
        return allSequences.Where(sequence => sequence.Count >= minSequenceLength).ToList();
    }

    //Find sequence around start param value
    private static List<int> FindLongestSequenceIncludingValue(Dictionary<int, int> itemDict, int value)
    {
        var result = new List<int>();
        //check if num exists in dictionary
        if (!itemDict.ContainsKey(value))
            return result;

        //initialize sequence list
        result.Add(value);

        //find values greater than starting value
        //and add to end of sequence
        var indexUp = value + 1;
        while (itemDict.ContainsKey(indexUp))
        {
            result.Add(itemDict[indexUp]);
            indexUp++;
        }

        //find values lower than starting value 
        //and add to start of sequence
        var indexDown = value - 1;
        while (itemDict.ContainsKey(indexDown))
        {
            result.Insert(0, itemDict[indexDown]);
            indexDown--;
        }
        return result;
    }

Here's a solution using a Dictionary instead of a sort...
It adds the items to a Dictionary, and then for each value increments above and below to find the longest sequence.
It is not strictly LINQ, though it does make use of some LINQ functions, and I think it is more readable than a pure LINQ solution..

static void Main(string[] args)
    {
        var items = new[] { -1, 0, 1, 21, -2, 4, 7, 9, 12, 22, 17, 8, 2, 20, 23 };
        IEnumerable<IEnumerable<int>> sequences = FindSequences(items, 3);

        foreach (var sequence in sequences)
        {   //print results to consol
            Console.Out.WriteLine(sequence.Select(num => num.ToString()).Aggregate((a, b) => a + "," + b));
        }
        Console.ReadLine();
    }

    private static IEnumerable<IEnumerable<int>> FindSequences(IEnumerable<int> items, int minSequenceLength)
    {
        //Convert item list to dictionary
        var itemDict = new Dictionary<int, int>();
        foreach (int val in items)
        {
            itemDict[val] = val;
        }
        var allSequences = new List<List<int>>();
        //for each val in items, find longest sequence including that value
        foreach (var item in items)
        {
            var sequence = FindLongestSequenceIncludingValue(itemDict, item);
            allSequences.Add(sequence);
            //remove items from dict to prevent duplicate sequences
            sequence.ForEach(i => itemDict.Remove(i));
        }
        //return only sequences longer than 3
        return allSequences.Where(sequence => sequence.Count >= minSequenceLength).ToList();
    }

    //Find sequence around start param value
    private static List<int> FindLongestSequenceIncludingValue(Dictionary<int, int> itemDict, int value)
    {
        var result = new List<int>();
        //check if num exists in dictionary
        if (!itemDict.ContainsKey(value))
            return result;

        //initialize sequence list
        result.Add(value);

        //find values greater than starting value
        //and add to end of sequence
        var indexUp = value + 1;
        while (itemDict.ContainsKey(indexUp))
        {
            result.Add(itemDict[indexUp]);
            indexUp++;
        }

        //find values lower than starting value 
        //and add to start of sequence
        var indexDown = value - 1;
        while (itemDict.ContainsKey(indexDown))
        {
            result.Insert(0, itemDict[indexDown]);
            indexDown--;
        }
        return result;
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文