LINQを使用してシーケンスの最後の要素を除くすべてを取得する方法は?


131

シーケンスがあるとしましょう。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();
// sequence now contains: 0,1,2,3,...,999999,1000000

シーケンスを取得するのは安くなく、動的に生成されるので、1回だけ繰り返し処理したい

0-999999(つまり、最後の要素以外のすべて)を取得したい

私は次のようなことができることを認識しています:

sequence.Take(sequence.Count() - 1);

しかし、その結果、大きなシーケンスに対して2つの列挙が生じます。

私ができるLINQ構造はありますか:

sequence.TakeAllButTheLastElement();

3
O(2n)時間またはO(count)スペース効率アルゴリズムのどちらかを選択する必要があります。後者では、内部配列内のアイテムを移動する必要もあります。
Dykam 2009年

1
ダリオ、そうでない人のために説明してくれませんか?
alexn 2009年

次の重複する質問も参照してください:stackoverflow.com/q/4166493/240733
stakx-

コレクションをListに変換してからを呼び出すことで、結果をキャッシュすることになりましたsequenceList.RemoveAt(sequence.Count - 1);。私の場合、すべてのLINQ操作の後、それを配列に変換するか、IReadOnlyCollectionとにかく変換する必要があるので、それは受け入れられます。キャッシングすら考慮しないユースケースは何でしょうか?私が見ることができるように、承認された答えでさえいくつかのキャッシュを行うので、への単純な変換Listははるかに簡単で短いと私は考えています。
Pavels Ahmadulins

回答:


64

Linqソリューションはわかりません-しかし、ジェネレーターを使用して自分でアルゴリズムを簡単にコーディングできます(yield return)。

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source) {
    var it = source.GetEnumerator();
    bool hasRemainingItems = false;
    bool isFirst = true;
    T item = default(T);

    do {
        hasRemainingItems = it.MoveNext();
        if (hasRemainingItems) {
            if (!isFirst) yield return item;
            item = it.Current;
            isFirst = false;
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 10);

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

または、最後のnアイテムを破棄する一般化されたソリューションとして(コメントで提案されているようなキューを使用):

public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n) {
    var  it = source.GetEnumerator();
    bool hasRemainingItems = false;
    var  cache = new Queue<T>(n + 1);

    do {
        if (hasRemainingItems = it.MoveNext()) {
            cache.Enqueue(it.Current);
            if (cache.Count > n)
                yield return cache.Dequeue();
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 4);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.SkipLastN(3).Select(x => x.ToString()).ToArray()));
}

7
最後のnを除くすべてを一般化できますか?
Eric Lippert、

4
いいね。1つの小さなエラー。キューのサイズはn + 1に初期化する必要があります。これは、キューの最大サイズだからです。
Eric Lippert、

ReSharperのは、あなたのコード(理解できない条件式での割り当てを)しかし、私ちょっとそれのような1
Грозный

44

独自のメソッドを作成する代わりに、要素の順序が重要でない場合は、次の方法が機能します。

var result = sequence.Reverse().Skip(1);

49
これには、シーケンス全体を格納するのに十分なメモリが必要です。もちろん、シーケンス全体を2回繰り返します。1回目は逆のシーケンスを構築し、もう1回は繰り返します。これは、どのようにスライスしても、Countソリューションよりもかなり悪いです。速度が遅く、メモリをはるかに多く消費します。
Eric Lippert、

'Reverse'メソッドがどのように機能するかわからないので、使用するメモリの量がわかりません。しかし、私は2つの反復について同意します。このメソッドは、大規模なコレクションやパフォーマンスが重要な場合には使用しないでください。
カマレイ2009年

5
さて、どのようにリバース実装しますか?シーケンス全体を保存せずにそれを行う一般的な方法を理解できますか?
Eric Lippert、

2
私はそれが好きで、シーケンスを2回生成しないという基準を満たしています。
エイミーB

12
さらに、シーケンス全体を逆にする必要もあります。シーケンスを維持するには、これがequence.Reverse().Skip(1).Reverse()適切な解決策ではないためです
shashwat

42

私は明示的にを使用するのが好きではないためEnumerator、代わりの方法を次に示します。ラッパーメソッドは、シーケンスが実際に列挙されるまでチェックを延期するのではなく、無効な引数を早期にスローさせるために必要です。

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");

    return InternalDropLast(source);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source)
{
    T buffer = default(T);
    bool buffered = false;

    foreach (T x in source)
    {
        if (buffered)
            yield return buffer;

        buffer = x;
        buffered = true;
    }
}

Eric Lippertの提案に従って、n個の項目に簡単に一般化します。

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("source");

    if (n < 0)
        throw new ArgumentOutOfRangeException("n", 
            "Argument n should be non-negative.");

    return InternalDropLast(source, n);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
    Queue<T> buffer = new Queue<T>(n + 1);

    foreach (T x in source)
    {
        buffer.Enqueue(x);

        if (buffer.Count == n + 1)
            yield return buffer.Dequeue();
    }
}

降格後ではなく降格前にバッファリングするため、n == 0特別な処理は必要ありません。


最初の例では、をbuffered=false割り当てる前に、else句を設定する方がはるかに高速ですbuffer。いずれにしても、条件は既にチェックされていますが、これbufferedにより、ループのたびに設定が冗長になるのを回避できます。
ジェームズ

誰かがこれと選択した答えの長所/短所を教えてもらえますか?
シンジャイ

入力チェックのない別のメソッドで実装することの利点は何ですか?また、単一の実装を削除して、複数の実装にデフォルト値を設定します。
jpmc26 2018

@ jpmc26別のメソッドでチェックを行うと、実際に呼び出した瞬間に検証が行われますDropLast。それ以外の場合、検証は実際にシーケンスを列挙したときにのみ行われます(MoveNext結果のに対する最初の呼び出し時IEnumerator)。を生成してIEnumerableから実際に列挙するまでの間に任意の量のコードが存在する可能性がある場合、デバッグするのは楽しいことではありません。現在、私はInternalDropLastの内部関数として記述しますがDropLast、9年前にこのコードを記述したとき、その機能はC#には存在しませんでした。
Joren

28

このEnumerable.SkipLast(IEnumerable<TSource>, Int32)メソッドは.NET Standard 2.1で追加されました。それはまさにあなたが望むものを行います。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplastから

ソースコレクションの最後のcount個の要素が省略された、ソースの要素を含む新しい列挙可能なコレクションを返します。


2
これは、MoreLinq
Leperkawnに

SkipLastの+1。私が最近.NET Frameworkから来て以来、私はこれを知りませんでした。
PRMan

12

BCL(または私が信じているMoreLinq)には何もありませんが、独自の拡張メソッドを作成できます。

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
        bool first = true;
        T prev;
        while(enumerator.MoveNext())
        {
            if (!first)
                yield return prev;
            first = false;
            prev = enumerator.Current;
        }
    }
}

このコードは、おそらくする必要があります...動作しませんif (!first)し、プルfirst = false場合のアウト。
カレブ

このコードは見落としています。ロジックは「初期化さprevれていない初期化を最初の反復で返し、その後永久にループする」ようです。
Phil Miller、

コードに「コンパイル時」エラーがあるようです。あなたはそれを修正したいと思うかもしれません。しかし、はい、1回反復して最後の項目を除くすべてを返すエクステンダーを作成できます。
Manish Basantani、2009年

@Caleb:あなたは絶対的に正しいです-私はこれを本当の急いで書きました!今修正されました。@Amby:えーと、使用しているコンパイラーがわかりません。私はそのようなものは何もありませんでした。:P
ノルドリン2009年

@RobertSchmidtおっと、はい。using今声明を追加しました。
Noldorin 2017年

7

.NET Frameworkにこのような拡張メソッドが同梱されていると便利です。

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    var enumerator = source.GetEnumerator();
    var queue = new Queue<T>(count + 1);

    while (true)
    {
        if (!enumerator.MoveNext())
            break;
        queue.Enqueue(enumerator.Current);
        if (queue.Count > count)
            yield return queue.Dequeue();
    }
}

3

ヨレンのエレガントなソリューションのわずかな拡張:

public static IEnumerable<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
{
    int i = 0;
    var buffer = new Queue<T>(right + 1);

    foreach (T x in source)
    {
        if (i >= left) // Read past left many elements at the start
        {
            buffer.Enqueue(x);
            if (buffer.Count > right) // Build a buffer to drop right many elements at the end
                yield return buffer.Dequeue();    
        } 
        else i++;
    }
}
public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(0, n);
}
public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(n, 0);
}

シュリンクは、最初のleft多くの要素を削除するための単純なカウントフォワードと、最後のright多くの要素を削除するための同じ破棄バッファーを実装します。


3

独自の拡張機能を展開する時間がない場合は、次の方法で簡単にできます。

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });

2

受け入れられた回答のわずかなバリエーション。これは(私の好みでは)少し簡単です。

    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        // for efficiency, handle degenerate n == 0 case separately 
        if (n == 0)
        {
            foreach (var item in enumerable)
                yield return item;
            yield break;
        }

        var queue = new Queue<T>(n);
        foreach (var item in enumerable)
        {
            if (queue.Count == n)
                yield return queue.Dequeue();

            queue.Enqueue(item);
        }
    }

2

Countor Lengthを取得できる場合は、ほとんどの場合はそれを取得できます。Take(n - 1)

配列の例

int[] arr = new int[] { 1, 2, 3, 4, 5 };
int[] sub = arr.Take(arr.Length - 1).ToArray();

の例 IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);

問題はIEnumerablesについてであり、OPが回避しようとしているのはソリューションです。コードはパフォーマンスに影響を与えます。
nawfal


1

この問題に使用する解決策はもう少し複雑です。

私のutil静的クラスにはMarkEndT-itemsの-items を変換する拡張メソッドが含まれていEndMarkedItem<T>ます。各要素にはint0である追加のマークが付けられます。または(最後の3アイテムに特に関心がある場合)-3-2、または-1、最後の3アイテム。

これは、それ自体が役立つ場合があります。たとえばforeach、最後の2を除く各要素の後にカンマを付けた単純なループでリストを作成する場合、最後から2番目の項目の後に結合語が続く(「および」など)または「または」)、およびポイントが続く最後の要素。

最後のnアイテムなしでリスト全体を生成するために、拡張メソッドButLastEndMarkedItem<T>sを単純に繰り返しながらEndMark == 0ます。

を指定しない場合tailLength、最後のアイテムのみがマーク(MarkEnd()または)またはドロップ(ButLast())されます。

他のソリューションと同様に、これはバッファリングによって機能します。

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

namespace Adhemar.Util.Linq {

    public struct EndMarkedItem<T> {
        public T Item { get; private set; }
        public int EndMark { get; private set; }

        public EndMarkedItem(T item, int endMark) : this() {
            Item = item;
            EndMark = endMark;
        }
    }

    public static class TailEnumerables {

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts) {
            return ts.ButLast(1);
        }

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts, int tailLength) {
            return ts.MarkEnd(tailLength).TakeWhile(te => te.EndMark == 0).Select(te => te.Item);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts) {
            return ts.MarkEnd(1);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts, int tailLength) {
            if (tailLength < 0) {
                throw new ArgumentOutOfRangeException("tailLength");
            }
            else if (tailLength == 0) {
                foreach (var t in ts) {
                    yield return new EndMarkedItem<T>(t, 0);
                }
            }
            else {
                var buffer = new T[tailLength];
                var index = -buffer.Length;
                foreach (var t in ts) {
                    if (index < 0) {
                        buffer[buffer.Length + index] = t;
                        index++;
                    }
                    else {
                        yield return new EndMarkedItem<T>(buffer[index], 0);
                        buffer[index] = t;
                        index++;
                        if (index == buffer.Length) {
                            index = 0;
                        }
                    }
                }
                if (index >= 0) {
                    for (var i = index; i < buffer.Length; i++) {
                        yield return new EndMarkedItem<T>(buffer[i], i - buffer.Length - index);
                    }
                    for (var j = 0; j < index; j++) {
                        yield return new EndMarkedItem<T>(buffer[j], j - index);
                    }
                }
                else {
                    for (var k = 0; k < buffer.Length + index; k++) {
                        yield return new EndMarkedItem<T>(buffer[k], k - buffer.Length - index);
                    }
                }
            }    
        }
    }
}

1
    public static IEnumerable<T> NoLast<T> (this IEnumerable<T> items) {
        if (items != null) {
            var e = items.GetEnumerator();
            if (e.MoveNext ()) {
                T head = e.Current;
                while (e.MoveNext ()) {
                    yield return head; ;
                    head = e.Current;
                }
            }
        }
    }

1

私はそれがこれより簡潔になるとは思わない-また、Disposeを確実にするIEnumerator<T>

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (var it = source.GetEnumerator())
    {
        if (it.MoveNext())
        {
            var item = it.Current;
            while (it.MoveNext())
            {
                yield return item;
                item = it.Current;
            }
        }
    }
}

編集:この回答と技術的に同一です。



0

あなたは書くことができます:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);

0

これは、すべてのケースを正しく処理する一般的でIMHOのエレガントなソリューションです。

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

public class Program
{
    public static void Main()
    {
        IEnumerable<int> r = Enumerable.Range(1, 20);
        foreach (int i in r.AllButLast(3))
            Console.WriteLine(i);

        Console.ReadKey();
    }
}

public static class LinqExt
{
    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        using (IEnumerator<T> enumerator = enumerable.GetEnumerator())
        {
            Queue<T> queue = new Queue<T>(n);

            for (int i = 0; i < n && enumerator.MoveNext(); i++)
                queue.Enqueue(enumerator.Current);

            while (enumerator.MoveNext())
            {
                queue.Enqueue(enumerator.Current);
                yield return queue.Dequeue();
            }
        }
    }
}

-1

私の伝統的なIEnumerableアプローチ:

/// <summary>
/// Skips first element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping first element</returns>
private IEnumerable<U> SkipFirstEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        for (;e.MoveNext();) yield return e.Current;
        yield return e.Current;
    }
}

/// <summary>
/// Skips last element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping last element</returns>
private IEnumerable<U> SkipLastEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        yield return e.Current;
        for (;e.MoveNext();) yield return e.Current;
    }
}

SkipLastEnumerableは従来のものである可能性がありますが、壊れています。それが返す最初の要素は、「モデル」に1つの要素がある場合でも、常に未定義のUです。後者の場合、空の結果が予想されます。
Robert Schmidt

また、IEnumerator <T>はIDisposableです。
Robert Schmidt

真/注目。ありがとうございました。
Chibueze Opata

-1

簡単な方法は、キューに変換して、スキップするアイテムの数だけが残るまでデキューすることです。

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int n)
{
    var queue = new Queue<T>(source);

    while (queue.Count() > n)
    {
        yield return queue.Dequeue();
    }
}

既知の数のアイテムを取得するために使用されるTakeがあります。そして、列挙可能な十分な大きさのキューはひどいです。
Sinatr

-2

になり得る:

var allBuLast = sequence.TakeWhile(e => e != sequence.Last());

de "Where"のように見えるはずですが、順序を保持しています(?)


3
これは非常に非効率的な方法です。sequence.Last()を評価するには、シーケンス全体をトラバースする必要があります。これは、シーケンス内の要素ごとに行います。O(n ^ 2)効率。
マイク

あなたが正しいです。このXDを書いたときに何を考えていたのかわかりません。とにかく、Last()がシーケンス全体をトラバースすることを確信していますか?IEnumerableの一部の実装(Arrayなど)では、これはO(1)である必要があります。私はリストの実装をチェックしませんでしたが、最後の要素から始まる「リバース」イテレータもある可能性があり、これもO(1)を取ります。また、少なくとも技術的に言えば、O(n)= O(2n)であることを考慮する必要があります。したがって、この手順がアプリのパフォーマンスに対して絶対的に批判的でない場合は、より明確なシーケンスを使用します.Take(sequence.Count()-1)。よろしくお願いします。
ギジェルモアレス

@Mike私はあなたと一致しません、sequence.Last()はO(1)なので、シーケンス全体をトラバースする必要はありません。stackoverflow.com/a/1377895/812598
GoRoS 2014年

1
@GoRoS、シーケンスがICollection / IListを実装している場合、または配列の場合はO(1)のみです。他のすべてのシーケンスはO(N)です。私の質問では、O(1)ソースの1つであるとは想定していません
マイク

3
シーケンスには、この条件を満たすe == sequence.Last()のような複数のアイテムが含まれる場合があります。たとえば、new [] {
1、1、1、1

-2

速度が要件である場合、コードがlinqで実現できるほど滑らかではない場合でも、この古い方法が最も高速です。

int[] newSequence = int[sequence.Length - 1];
for (int x = 0; x < sequence.Length - 1; x++)
{
    newSequence[x] = sequence[x];
}

シーケンスには固定長とインデックス付きのアイテムがあるため、シーケンスは配列である必要があります。


2
インデックスを介した要素へのアクセスを許可しないIEnumerableを処理しています。あなたのソリューションは機能しません。正しく実行すると仮定すると、シーケンスを走査して長さを決定し、長さn-1の配列を割り当てて、すべての要素をコピーする必要があります。-1. 2n-1演算と(2n-1)*(4または8バイト)のメモリ。それも速くはありません。
Tarik、

-4

私はおそらくこのようなことをするでしょう:

sequence.Where(x => x != sequence.LastOrDefault())

これは、毎回最後ではないことを確認する1回の反復です。


3
これは良いことではない2つの理由です。1).LastOrDefault()はシーケンス全体を繰り返す必要があり、シーケンスの各要素(.Where()内)に対して呼び出されます。2)シーケンスが[1,2,1,2,1,2]で、テクニックを使用した場合、[1,1,1]のままになります。
Mike
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.