Jak wziąć wszystkie elementy oprócz ostatniego w sekwencji przy użyciu LINQ?


136

Powiedzmy, że mam sekwencję.

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

Uzyskanie sekwencji nie jest tanie i jest generowane dynamicznie, a ja chcę ją powtórzyć tylko raz.

Chcę dostać 0 - 999999 (czyli wszystko oprócz ostatniego elementu)

Rozumiem, że mógłbym zrobić coś takiego:

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

ale to skutkuje dwoma wyliczeniami w dużej sekwencji.

Czy istnieje konstrukcja LINQ, która pozwala mi to zrobić:

sequence.TakeAllButTheLastElement();

3
Musisz wybrać między algorytmem wydajności przestrzeni O (2n) czas lub O (liczba), gdzie ten ostatni musi również przesuwać elementy w wewnętrznej tablicy.
Dykam

1
Dario, czy mógłbyś wyjaśnić komuś, kto nie lubi wielkiej o-notacji?
alexn

Zobacz także to zduplikowane pytanie: stackoverflow.com/q/4166493/240733
stakx - już nie wysyłam

Skończyło się na tym, że zapisałem go w pamięci podręcznej, konwertując kolekcję na List, a następnie dzwoniąc sequenceList.RemoveAt(sequence.Count - 1);. W moim przypadku jest to dopuszczalne, ponieważ po wszystkich manipulacjach LINQ muszę przekonwertować go na tablicę lub IReadOnlyCollectiontak czy inaczej. Zastanawiam się, jaki jest twój przypadek użycia, w którym nawet nie rozważasz buforowania? Jak widzę, nawet zatwierdzona odpowiedź powoduje pewne buforowanie, więc Listmoim zdaniem prosta konwersja do jest znacznie łatwiejsza i krótsza.
Pavels Ahmadulins

Odpowiedzi:


65

Nie znam rozwiązania Linq - ale możesz łatwo samodzielnie zakodować algorytm za pomocą generatorów (zwrot zysku).

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

Lub jako uogólnione rozwiązanie odrzucające ostatnie n pozycji (używając kolejki, jak sugerowano w komentarzach):

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
Czy możesz teraz uogólnić, aby wziąć wszystko oprócz ostatniego n?
Eric Lippert

4
Ładny. Jeden mały błąd; rozmiar kolejki powinien być zainicjowany na n + 1, ponieważ jest to maksymalny rozmiar kolejki.
Eric Lippert

ReSharper nie rozumie twojego kodu ( przypisanie w wyrażeniu warunkowym ), ale trochę mi się podoba +1
Иван Грозный

44

Jako alternatywa dla tworzenia własnej metody iw przypadku gdy kolejność elementów nie jest ważna, zadziała następna:

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

49
Zauważ, że wymaga to wystarczającej ilości pamięci, aby zapisać całą sekwencję i oczywiście NADAL iteruje całą sekwencję dwukrotnie, raz w celu zbudowania odwróconej sekwencji i raz w celu jej iteracji. Jest to znacznie gorsze niż rozwiązanie Count, bez względu na to, jak je pokroisz; jest wolniejszy ORAZ zajmuje dużo więcej pamięci.
Eric Lippert

Nie wiem, jak działa metoda „odwrotna”, więc nie jestem pewien, ile pamięci zajmuje. Ale zgadzam się co do dwóch iteracji. Ta metoda nie powinna być używana w przypadku dużych kolekcji lub jeśli wydajność jest ważna.
Kamarey

5
Cóż, w jaki sposób możesz wdrożyć Do tyłu? Czy możesz ogólnie znaleźć sposób, aby to zrobić bez przechowywania całej sekwencji?
Eric Lippert

2
Podoba mi się i spełnia kryteria nie generowania sekwencji dwukrotnie.
Amy B

12
a dodatkowo będziesz musiał ponownie odwrócić całą sekwencję, aby ją zachować, ponieważ nie jest equence.Reverse().Skip(1).Reverse()to dobre rozwiązanie
shashwat

42

Ponieważ nie jestem fanem jawnego używania an Enumerator, oto alternatywa. Należy zauważyć, że metody opakowujące są potrzebne, aby umożliwić wczesne wyrzucanie nieprawidłowych argumentów, zamiast odkładać sprawdzanie do momentu faktycznego wyliczenia sekwencji.

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

Zgodnie z sugestią Erica Lipperta łatwo uogólnia się na n pozycji:

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

Gdzie teraz buforuję przed poddaniem zamiast po ustąpieniu, aby n == 0sprawa nie wymagała specjalnego traktowania.


W pierwszym przykładzie byłoby prawdopodobnie nieco szybciej ustawienie buffered=falsew klauzuli else przed przypisaniem buffer. Stan i tak jest już sprawdzany, ale pozwoliłoby to uniknąć zbędnego ustawiania za bufferedkażdym razem przez pętlę.
James

Czy ktoś może mi powiedzieć plusy / minusy tego w porównaniu z wybraną odpowiedzią?
Sinjai,

Jaka jest zaleta posiadania implementacji w oddzielnej metodzie, w której brakuje sprawdzania danych wejściowych? Poza tym porzuciłbym pojedynczą implementację i nadał wielu implementacjom domyślną wartość.
jpmc26

@ jpmc26 Dzięki sprawdzaniu w oddzielnej metodzie w rzeczywistości otrzymujesz walidację w momencie wywołania DropLast. W przeciwnym razie walidacja ma miejsce tylko wtedy, gdy faktycznie wyliczysz sekwencję (przy pierwszym wywołaniu MoveNextwyniku IEnumerator). Debugowanie nie jest przyjemną rzeczą, gdy między wygenerowaniem IEnumerablea wyliczeniem może być dowolna ilość kodu . Obecnie pisałbym InternalDropLastjako wewnętrzna funkcja programu DropLast, ale ta funkcjonalność nie istniała w C #, kiedy pisałem ten kod 9 lat temu.
Joren

33

Enumerable.SkipLast(IEnumerable<TSource>, Int32)Sposób dodano NET standardu 2.1. Robi dokładnie to, czego chcesz.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Zwraca nową wyliczalną kolekcję, która zawiera elementy ze źródła z pominiętymi ostatnimi elementami count kolekcji źródłowej.


2
To również istnieje w
MoreLinq

+1 dla SkipLast. Nie wiedziałem o tym, ponieważ niedawno wyszedłem z .NET Framework i nie zadali sobie trudu, aby go tam dodać.
PRMan

12

Nic w BCL (lub wierzę, że MoreLinq), ale możesz stworzyć własną metodę rozszerzenia.

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

Ten kod nie zadziała ... prawdopodobnie powinien być if (!first)i wyciągnąć first = falsez if.
Caleb

Ten kod wygląda na wyłączony. Wydaje się, że logika brzmi: „zwróć niezainicjowane elementy prevw pierwszej iteracji i zapętlaj się na zawsze”.
Phil Miller

Wygląda na to, że kod zawiera błąd „czasu kompilacji”. Może chciałbyś to poprawić. Ale tak, możemy napisać rozszerzenie, które wykonuje iterację raz i zwraca wszystko oprócz ostatniego.
Manish Basantani

@Caleb: Masz całkowitą rację - napisałem to w pośpiechu! Naprawiono teraz. @Amby: Erm, nie jestem pewien, jakiego kompilatora używasz. Nie miałem nic takiego. : P
Noldorin

@RobertSchmidt Ups, tak. Dodałem usingteraz oświadczenie.
Noldorin

7

Byłoby pomocne, gdyby .NET Framework został dostarczony z taką metodą rozszerzenia.

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

Nieznaczne rozwinięcie eleganckiego rozwiązania Jorena:

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

Gdzie shrink implementuje proste liczenie do przodu, aby porzucić pierwsze leftwiele elementów i ten sam odrzucony bufor, aby usunąć ostatnie rightwiele elementów.


3

jeśli nie masz czasu na wprowadzenie własnego rozszerzenia, oto szybszy sposób:

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

3

Jeśli możesz uzyskać Countlub Lengthz wyliczalnych, co w większości przypadków możesz, to po prostuTake(n - 1)

Przykład z tablicami

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

Przykład z IEnumerable<T>

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

Pytanie dotyczy IEnumerables, a Twoim rozwiązaniem jest to, czego OP stara się uniknąć. Twój kod ma wpływ na wydajność.
nawfal

2

Niewielka wariacja na temat zaakceptowanej odpowiedzi, która (jak na mój gust) jest nieco prostsza:

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


1

Dlaczego nie po prostu .ToList<type>()na sekwencji, a potem liczenie połączeń i przyjmowanie, tak jak pierwotnie, ale ponieważ zostało wciągnięte do listy, nie powinno dwukrotnie wykonywać kosztownego wyliczenia. Dobrze?


1

Rozwiązanie, którego używam do tego problemu, jest nieco bardziej rozbudowane.

Moja klasa statyczna util zawiera metodę rozszerzenia, MarkEndktóra konwertuje T-items w EndMarkedItem<T>-items. Każdy element jest oznaczony dodatkowym int, którym jest 0 ; lub (w przypadku, gdy ktoś jest szczególnie zainteresowany 3 ostatnimi pozycjami) -3 , -2 lub -1 dla ostatnich 3 pozycji.

Może to być przydatne samo w sobie, np. Gdy chcesz utworzyć listę w prostej foreachpętli z przecinkami po każdym elemencie z wyjątkiem ostatnich 2, z przedostatnim elementem, po którym następuje łącznik (taki jak „ i ” lub „ lub ”) i ostatni element, po którym następuje punkt.

Aby wygenerować całą listę bez ostatnich n elementów, metoda rozszerzenia ButLastpo prostu wykonuje iterację po EndMarkedItem<T>s while EndMark == 0.

Jeśli nie określisz tailLength, tylko ostatnia pozycja zostanie oznaczona (w MarkEnd()) lub upuszczona (w ButLast()).

Podobnie jak inne rozwiązania, działa to przez buforowanie.

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

Nie sądzę, że może to być bardziej zwięzłe niż to - zapewniając również Pozbywanie się 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;
            }
        }
    }
}

Edycja: technicznie identyczna z tą odpowiedzią .


0

Możesz napisać:

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

0

Jest to ogólne i eleganckie rozwiązanie IMHO, które poprawnie obsłuży wszystkie przypadki:

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

Moje tradycyjne IEnumerablepodejście:

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

Twój SkipLastEnumerable może być tradycyjny, ale jest uszkodzony. Pierwszym zwracanym elementem jest zawsze niezdefiniowane U - nawet jeśli „modele” mają 1 element. W tym drugim przypadku spodziewałbym się pustego wyniku.
Robert Schmidt

Ponadto IEnumerator <T> jest IDisposable.
Robert Schmidt

Prawda / odnotowana. Dziękuję Ci.
Chibueze Opata

-1

Prostym sposobem byłoby po prostu przekonwertowanie na kolejkę i usunięcie z kolejki, aż pozostanie tylko liczba elementów, które chcesz pominąć.

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

Jest Take wykorzystywane do podejmowania określonej liczbie elementów. A kolejka do wystarczająco dużych wyliczalnych jest okropna.
Sinatr

-2

Możliwe:

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

Myślę, że powinno to być jak de "Gdzie", ale z zachowaniem kolejności (?).


3
To bardzo nieefektywny sposób, aby to zrobić. Aby ocenić sekwencję.Last (), musi przejść przez całą sekwencję, robiąc to dla każdego elementu w sekwencji. Wydajność O (n ^ 2).
Mike

Masz rację. Nie wiem, o czym myślałem, kiedy pisałem ten XD. W każdym razie, czy jesteś pewien, że Last () przejdzie przez całą sekwencję? W przypadku niektórych implementacji IEnumerable (takich jak Array) powinno to być O (1). Nie sprawdziłem implementacji List, ale mogłaby mieć też iterator „odwrotny”, zaczynający się w ostatnim elemencie, który również zająłby O (1). Należy również wziąć pod uwagę, że O (n) = O (2n), przynajmniej z technicznego punktu widzenia. Dlatego jeśli ta procedura nie jest absolutnie krytyczna dla wydajności twoich aplikacji, trzymałbym się znacznie jaśniejszej sekwencji. Weź (sequence.Count () - 1).
Guillermo Ares

@Mike Nie zgadzam się z tobą kolego, sequence.Last () to O (1), więc nie musi przechodzić przez całą sekwencję. stackoverflow.com/a/1377895/812598
GoRoS

1
@GoRoS, to tylko O ​​(1), jeśli sekwencja implementuje ICollection / IList lub jest tablicą. Wszystkie inne sekwencje to O (N). W moim pytaniu nie zakładam, że jedno ze źródeł O (1)
Mike

3
Sekwencja może mieć kilka elementów spełniających ten warunek e == sequence.Last (), na przykład new [] {1, 1, 1, 1}
Sergey Shandar

-2

Jeśli wymagana jest szybkość, ta stara metoda powinna być najszybsza, nawet jeśli kod nie wygląda tak gładko, jak mógłby to zrobić linq.

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

Wymaga to, aby sekwencja była tablicą, ponieważ ma stałą długość i indeksowane elementy.


2
Masz do czynienia z IEnumerable, który nie zezwala na dostęp do elementów za pośrednictwem indeksu. Twoje rozwiązanie nie działa. Zakładając, że zrobisz to dobrze, wymaga przejścia sekwencji w celu określenia długości, przydzielenia tablicy o długości n-1, skopiowania wszystkich elementów. - 1. operacje 2n-1 i (2n-1) * (4 lub 8 bajtów) pamięci. To nawet nie jest szybkie.
Tarik

-4

Prawdopodobnie zrobiłbym coś takiego:

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

Jest to jedna iteracja ze sprawdzeniem, czy nie jest to ostatnia za każdym razem.


3
Z dwóch powodów nie warto tego robić. 1) .LastOrDefault () wymaga iteracji całej sekwencji i jest to wywoływane dla każdego elementu w sekwencji (w .Where ()). 2) Jeśli sekwencja to [1,2,1,2,1,2] i użyłeś swojej techniki, zostanie Ci [1,1,1].
Mike
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.