25

21,4,7,9,12,22,17,8,2,20,23などの数字のリストがあります

一連の連続番号 (最小 3 項目の長さ) を選択できるようにしたいので、上記の例では 7,8,9 と 20,21,22,23 になります。

私はいくつかの醜いスプロール関数をいじってみましたが、それを行うためのきちんとした LINQ 風の方法があるかどうか疑問に思っています。

助言がありますか?

アップデート:

すべての回答に感謝します。私は現在、それらすべてを試して、どれが私たちのプロジェクトに最もよく統合されるかを確認しています.

4

12 に答える 12

28

最初にすべきことは、リストを並べ替えることだと思います。次に、現在のシーケンスの長さを記憶し、いつ終了したかを検出して、それをウォークスルーするだけです。正直なところ、単純な 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、current、previous) のタプルを取得します。
  • (current = previous + 1) のアイテムを除外して、シーケンスの開始として数えられる場所を取得します (特殊なケースのインデックス = 0)
  • SelectWithPrevious結果に対して使用して、 2 つの開始点間のシーケンスの長さを取得します (前のインデックスから 1 つのインデックスを減算します)。
  • 長さが 3 未満のシーケンスを除外します

最終的なアイテムが適切に使用されることを保証するために、順序付けられたシーケンス連結する必要があると思います。int.MinValue

編集:さて、私はこれを実装しました。これは、これを行うために私が考えることができるLINQiest方法についてです...開始シーケンスと終了シーケンスを強制するために、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));
    }
}
于 2010-10-02T06:20:26.737 に答える
14

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クエリのパフォーマンスは、 (改善するために)入力を にロードすることでわずかに改善できますがContains、それでも効率に近いソリューションは得られません。

私が認識している唯一のバグは、シーケンスに大きなcount負の数が含まれている場合に算術オーバーフローが発生する可能性があることです ( のパラメーターを表すことはできませんRange)。static IEnumerable<int> 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));
于 2010-10-02T10:17:56.057 に答える
4

私のソリューションはよりエレガントでシンプルなので、正しいことを確認しやすいと思います:

/// <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);
}

他のソリューションに対する利点:

  • 重複するシーケンスを見つけることができます。
  • 適切に再利用可能で、文書化されています。
  • バグは見つかりませんでした;-)
于 2010-10-02T09:49:44.593 に答える
2

「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()));

アレイのコピーと再構築のため、このソリューションは、もちろん、ループを使用する従来のソリューションほど効率的ではありません。

于 2010-10-02T07:18:02.610 に答える
1

これが私のショットです:

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()));
        }
    }
}

これにより、サブシーケンスを形成する特定のアイテムが返され、隣接するアイテムを比較して決定できる限り、任意のタイプのアイテムと基準の定義が許可されます。

于 2010-10-03T00:04:45.437 に答える
1

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()));
}
于 2010-10-02T17:49:38.700 に答える
1

配列を並べ替えてから、前の要素と各要素の違いである別の配列を作成するのはどうですか


sortedArray = 8, 9, 10, 21, 22, 23, 24, 27, 30, 31, 32
diffArray   =    1,  1, 11,  1,  1,  1,  3,  3,  1,  1
差分配列を反復処理します。差が 1 の場合、変数 sequenceLength のカウントを 1 増やします。差が > 1 の場合、sequenceLength をチェックして >=2 の場合、少なくとも 3 つの連続する要素のシーケンスがあります。次に、sequenceLenght を 0 にリセットし、差分配列でループを続行します。

于 2010-10-02T18:44:41.613 に答える
0

これが私の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))
于 2010-10-02T20:57:24.107 に答える
0

並べ替えの代わりにディクショナリを使用するソリューションを次に示します...アイテムをディクショナリに追加し、値ごとに上下に増分して最長のシーケンスを見つけます。
厳密には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;
    }
于 2010-10-02T21:01:17.717 に答える
0

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();
}
于 2010-10-02T19:49:39.210 に答える
0

F# で作成したソリューションを次に示します。fold は LINQ 集計演算子とほとんど同じであるため、これを C# 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
于 2010-10-02T19:46:43.193 に答える
0

私はJonと同じことを考えました: 連続する整数の範囲を表すために本当に必要なのは 2 つのごくわずかな整数だけです! だから私はそこから始めます:

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
于 2010-10-02T20:03:03.107 に答える