Bagaimana cara mengambil semua kecuali elemen terakhir secara berurutan menggunakan LINQ?


131

Katakanlah saya memiliki urutan.

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

Mendapatkan urutan tidak murah dan dihasilkan secara dinamis, dan saya ingin mengulanginya sekali saja.

Saya ingin mendapatkan 0 - 999999 (yaitu semuanya kecuali elemen terakhir)

Saya menyadari bahwa saya dapat melakukan sesuatu seperti:

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

tetapi itu menghasilkan dua enumerasi dalam urutan besar.

Apakah ada konstruksi LINQ yang memungkinkan saya melakukannya:

sequence.TakeAllButTheLastElement();

3
Anda harus memilih antara algoritma efisiensi ruang O (2n) atau O (hitung), di mana yang terakhir juga perlu memindahkan item dalam array internal.
Dykam

1
Dario, tolong jelaskan untuk seseorang yang tidak terlalu suka notasi?
alexn


Saya berakhir dengan caching dengan mengkonversi koleksi ke Daftar dan kemudian menelepon sequenceList.RemoveAt(sequence.Count - 1);. Dalam kasus saya ini dapat diterima karena bagaimanapun juga manipulasi LINQ saya harus mengubahnya menjadi array atau IReadOnlyCollectiontetap. Saya ingin tahu apa kasus penggunaan Anda di mana Anda bahkan tidak mempertimbangkan caching? Seperti yang saya lihat, bahkan jawaban yang disetujui melakukan caching sehingga konversi yang sederhana menjadi Listlebih mudah dan lebih pendek menurut saya.
Pavels Ahmadulins

Jawaban:


64

Saya tidak tahu solusi Linq - Tapi Anda dapat dengan mudah kode algoritma sendiri menggunakan generator (hasil pengembalian).

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

Atau sebagai solusi umum membuang n item terakhir (menggunakan antrian seperti yang disarankan dalam komentar):

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
Sekarang bisakah Anda menggeneralisasi untuk mengambil semua kecuali n final?
Eric Lippert

4
Bagus. Satu kesalahan kecil; ukuran antrian harus diinisialisasi ke n +1 karena itu adalah ukuran maksimum antrian.
Eric Lippert

ReSharper tidak mengerti kode Anda ( penugasan dalam ekspresi bersyarat ) tetapi saya agak suka +1
Грозный

44

Sebagai alternatif untuk membuat metode Anda sendiri dan dalam kasus urutan elemen tidak penting, selanjutnya akan berfungsi:

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

49
Perhatikan bahwa ini mengharuskan Anda memiliki cukup memori untuk menyimpan seluruh urutan, dan tentu saja MASIH mengulangi seluruh urutan dua kali, sekali untuk membangun urutan terbalik dan satu kali untuk mengulanginya. Cukup banyak ini lebih buruk daripada solusi Count tidak peduli bagaimana Anda mengirisnya; lebih lambat DAN membutuhkan lebih banyak memori.
Eric Lippert

Saya tidak tahu bagaimana metode 'Reverse' bekerja, jadi saya tidak yakin tentang jumlah memori yang digunakannya. Tapi saya setuju tentang dua iterasi. Metode ini tidak boleh digunakan pada koleksi besar atau jika kinerja itu penting.
Kamarey

5
Nah, bagaimana Anda mengimplementasikan Reverse? Bisakah Anda mencari cara secara umum untuk melakukannya tanpa menyimpan seluruh urutan?
Eric Lippert

2
Saya menyukainya, dan memenuhi kriteria tidak menghasilkan urutan dua kali.
Amy B

12
dan selain itu Anda juga perlu membalik seluruh urutan lagi untuk menjaganya karena itu equence.Reverse().Skip(1).Reverse()bukan solusi yang baik
shashwat

42

Karena saya bukan penggemar penggunaan eksplisit Enumerator, inilah alternatifnya. Perhatikan bahwa metode pembungkus diperlukan untuk membiarkan argumen yang tidak valid membuang lebih awal, daripada menunda pemeriksaan sampai urutan sebenarnya disebutkan.

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

Sesuai saran Eric Lippert, ini dengan mudah digeneralisasikan ke n item:

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

Di mana saya sekarang buffer sebelum menghasilkan bukan setelah menghasilkan, sehingga n == 0kasing tidak perlu penanganan khusus.


Dalam contoh pertama, mungkin akan sedikit lebih cepat untuk mengatur buffered=falseklausa lain sebelum menetapkan buffer. Kondisi ini sudah diperiksa, tetapi ini akan menghindari pengaturan yang berlebihan bufferedsetiap kali melalui loop.
James

Dapatkah seseorang memberi tahu saya pro / kontra dari ini versus jawaban yang dipilih?
Sinjai

Apa keuntungan memiliki implementasi dalam metode terpisah yang tidak memiliki pemeriksaan input? Selain itu, saya baru saja menjatuhkan implementasi tunggal dan memberikan beberapa implementasi nilai default.
jpmc26

@ jpmc26 Dengan cek dalam metode terpisah, Anda benar-benar mendapatkan validasi saat Anda menelepon DropLast. Kalau tidak, validasi hanya terjadi ketika Anda benar-benar menghitung urutan (pada panggilan pertama ke MoveNexthasil IEnumerator). Bukan hal yang menyenangkan untuk di-debug ketika mungkin ada jumlah kode yang berubah-ubah antara membuat IEnumerabledan benar-benar menghitungnya. Saat ini saya akan menulis InternalDropLastsebagai fungsi dalam DropLast, tetapi fungsi itu tidak ada di C # ketika saya menulis kode ini 9 tahun yang lalu.
Joren

28

The Enumerable.SkipLast(IEnumerable<TSource>, Int32)Metode ditambahkan dalam NET Standard 2.1. Itu tepat seperti yang Anda inginkan.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Mengembalikan koleksi enumerable baru yang berisi elemen dari sumber dengan elemen hitungan terakhir dari koleksi sumber dihilangkan.


2
Ini juga ada di MoreLinq
Leperkawn

+1 untuk SkipLast. Saya tidak tahu ini karena saya baru saja datang dari .NET Framework dan mereka tidak repot-repot menambahkannya di sana.
PRMan

12

Tidak ada dalam BCL (atau MoreLinq saya percaya), tetapi Anda dapat membuat metode ekstensi Anda sendiri.

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

Kode ini tidak akan berfungsi ... mungkin harus if (!first)dan first = falsekeluar dari if.
Caleb

Kode ini tidak aktif. Logikanya tampaknya 'mengembalikan yang tidak diinisialisasi prevdalam iterasi pertama, dan mengulang selamanya setelah itu'.
Phil Miller

Kode tampaknya memiliki kesalahan "waktu kompilasi". Mungkin Anda ingin memperbaikinya. Tapi ya, kita bisa menulis extender yang diulang sekali dan mengembalikan semua kecuali item terakhir.
Manish Basantani

@ Caleb: Anda memang benar - saya menulis ini dengan sangat cepat! Diperbaiki sekarang @Amby: Erm, tidak yakin kompiler apa yang Anda gunakan. Saya tidak punya hal semacam itu. : P
Noldorin

@RobertSchmidt Ups, ya. Saya menambahkan usingpernyataan sekarang.
Noldorin

7

Akan sangat membantu jika .NET Framework dikirimkan dengan metode ekstensi seperti ini.

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

Sedikit ekspansi pada solusi Joren yang elegan:

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

Di mana shrink mengimplementasikan penghitungan sederhana ke depan untuk menjatuhkan leftbanyak elemen pertama dan buffer buangan yang sama untuk menjatuhkan rightbanyak elemen terakhir .


3

jika Anda tidak punya waktu untuk meluncurkan ekstensi Anda sendiri, inilah cara yang lebih cepat:

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

2

Sedikit variasi pada jawaban yang diterima, yang (sesuai selera saya) sedikit lebih sederhana:

    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

Jika Anda bisa mendapatkan Countatau Lengthmenghitung, yang dalam banyak kasus Anda bisa, maka adilTake(n - 1)

Contoh dengan array

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

Contoh dengan IEnumerable<T>

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

Pertanyaannya adalah tentang IEnumerables dan solusi Anda adalah apa yang OP coba hindari. Kode Anda memiliki dampak kinerja.
nawfal

1

Mengapa tidak hanya .ToList<type>()pada urutan, lalu panggil hitungan dan ambil seperti yang Anda lakukan pada awalnya..tapi karena sudah ditarik ke dalam daftar, seharusnya tidak melakukan penghitungan mahal dua kali. Baik?


1

Solusi yang saya gunakan untuk masalah ini sedikit lebih rumit.

Kelas statis util saya berisi metode ekstensi MarkEndyang mengubah T-items di EndMarkedItem<T>-items. Setiap elemen ditandai dengan tambahan int, yaitu 0 ; atau (dalam kasus satu sangat tertarik pada 3 item terakhir) -3 , -2 , atau -1 untuk 3 item terakhir.

Ini bisa berguna sendiri, misalnya ketika Anda ingin membuat daftar di foreach-loop sederhana dengan koma setelah setiap elemen kecuali 2 terakhir, dengan item kedua-terakhir diikuti oleh kata penghubung (seperti " dan " atau " atau "), dan elemen terakhir diikuti oleh titik.

Untuk menghasilkan seluruh daftar tanpa n item terakhir , metode ekstensi ButLasthanya mengulang selama beberapa EndMarkedItem<T>saat EndMark == 0.

Jika Anda tidak menentukan tailLength, hanya item terakhir yang ditandai (masuk MarkEnd()) atau dijatuhkan (masuk ButLast()).

Seperti solusi lain, ini bekerja dengan buffering.

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

Saya tidak berpikir itu bisa menjadi lebih ringkas dari ini - juga memastikan untuk Buang 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;
            }
        }
    }
}

Sunting: secara teknis identik dengan jawaban ini .


1

Dengan C # 8.0 Anda dapat menggunakan Rentang dan indeks untuk itu.

var allButLast = sequence[..^1];

Secara default C # 8.0 memerlukan .NET Core 3.0 atau .NET Standard 2.1 (atau lebih tinggi). Periksa utas ini untuk digunakan dengan implementasi yang lebih lama.


0

Anda bisa menulis:

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

0

Ini adalah solusi umum dan IMHO elegan yang akan menangani semua kasus dengan benar:

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

IEnumerablePendekatan tradisional saya :

/// <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 Anda mungkin tradisional, tetapi rusak. Elemen pertama yang dikembalikan selalu merupakan U yang tidak ditentukan - bahkan ketika "model" memiliki 1 elemen. Dalam kasus terakhir, saya mengharapkan hasil yang kosong.
Robert Schmidt

Juga, IEnumerator <T> adalah IDisposable.
Robert Schmidt

Benar / dicatat. Terima kasih.
Chibueze Opata

-1

Cara sederhana adalah dengan hanya mengkonversi ke antrian dan dequeue sampai hanya jumlah item yang ingin Anda lewati yang tersisa.

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

Ada Take digunakan untuk mengambil sejumlah item yang diketahui. Dan antrian untuk menghitung cukup besar itu mengerikan.
Sinatr

-2

Bisa jadi:

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

Saya kira itu harus seperti de "Di mana" tetapi menjaga urutan (?).


3
Itu cara yang sangat tidak efisien untuk melakukannya. Untuk mengevaluasi urutan. Terakhir () harus melintasi seluruh urutan, melakukannya untuk setiap elemen dalam urutan. O (n ^ 2) efisiensi.
Mike

Kamu benar. Saya tidak tahu apa yang saya pikirkan ketika saya menulis XD ini. Ngomong-ngomong, apakah Anda yakin Last () akan melintasi seluruh urutan? Untuk beberapa implementasi IEnumerable (seperti Array), ini harus O (1). Saya tidak memeriksa implementasi Daftar, tetapi juga bisa memiliki iterator "terbalik", dimulai pada elemen terakhir, yang akan mengambil O (1) juga. Juga, Anda harus memperhitungkan bahwa O (n) = O (2n), setidaknya secara teknis. Oleh karena itu, jika prosedur ini tidak sepenuhnya mengkritik kinerja aplikasi Anda, saya akan tetap dengan urutan yang lebih jelas. Ambil (sequence.Count () - 1) .Regard!
Guillermo Ares

@ Mike Saya tidak setuju dengan Anda sobat, urutan. Terakhir () adalah O (1) sehingga tidak perlu melintasi seluruh urutan. stackoverflow.com/a/1377895/812598
GoRoS

1
@ GoRoS, itu hanya O (1) jika urutan mengimplementasikan ICollection / IList atau adalah Array. Semua urutan lainnya adalah O (N). Dalam pertanyaan saya, saya tidak berasumsi bahwa salah satu sumber O (1)
Mike

3
Urutan mungkin memiliki beberapa item yang memenuhi kondisi ini e == sequence.Last (), misalnya baru [] {1, 1, 1, 1}
Sergey Shandar

-2

Jika kecepatan adalah persyaratan, cara jadul ini seharusnya menjadi yang tercepat, meskipun kodenya tidak semulus linq.

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

Ini mensyaratkan bahwa urutannya adalah array karena memiliki panjang yang tetap dan item yang diindeks.


2
Anda berhadapan dengan IEnumerable yang tidak memungkinkan akses ke elemen melalui indeks. Solusi Anda tidak berfungsi. Dengan asumsi Anda melakukannya dengan benar, itu memerlukan melintasi urutan untuk menentukan panjang, mengalokasikan array dengan panjang n-1, menyalin semua elemen. - 1. Pengoperasian 2n-1 dan (2n-1) * (4 atau 8 byte) memori. Itu bahkan tidak cepat.
Tarik

-4

Saya mungkin akan melakukan sesuatu seperti ini:

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

Ini adalah satu iterasi dengan cek bahwa itu bukan yang terakhir untuk setiap kali.


3
Dua alasan itu bukan hal yang baik untuk dilakukan. 1) .LastOrDefault () membutuhkan iterasi seluruh urutan, dan itu disebut untuk setiap elemen dalam urutan (di .Where ()). 2) Jika urutannya [1,2,1,2,1,2] dan Anda menggunakan teknik Anda, Anda akan dibiarkan dengan [1,1,1].
Mike
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.