Как взять все элементы последовательности, кроме последнего, с помощью LINQ?

131

Допустим, у меня есть последовательность.

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

Получение последовательности стоит недешево и генерируется динамически, и я хочу повторить ее только один раз.

Я хочу получить 0 - 999999 (т.е. все, кроме последнего элемента)

Я понимаю, что могу сделать что-то вроде:

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

но это приводит к двум перечислениям по большой последовательности.

Есть ли конструкция LINQ, которая позволяет мне:

sequence.TakeAllButTheLastElement();
Майк
источник
3
Вы должны выбирать между алгоритмом эффективности пространства O (2n) или O (count), где последний также должен перемещать элементы во внутреннем массиве.
Dykam
1
Дарио, не могли бы вы объяснить кому-нибудь, кто не очень любит большие оцифры?
alexn
См. Также этот повторяющийся вопрос: stackoverflow.com/q/4166493/240733
stakx - больше не участвует
Я закончил кеширование, преобразовав коллекцию в список, а затем позвонив sequenceList.RemoveAt(sequence.Count - 1);. В моем случае это приемлемо, потому что после всех манипуляций с LINQ мне нужно преобразовать его в массив или в IReadOnlyCollectionлюбом случае. Интересно, какой у вас вариант использования, когда вы даже не рассматриваете кеширование? Как я вижу, даже утвержденный ответ делает некоторое кеширование, поэтому простое преобразование, на Listмой взгляд, намного проще и короче.
Павел Ахмадулинс

Ответы:

64

Я не знаю решения Linq - но вы можете легко запрограммировать алгоритм самостоятельно, используя генераторы (доходность).

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()));
}
Dario
источник
7
Теперь вы можете обобщить это, чтобы взять все, кроме последнего n?
Эрик Липперт,
4
Ницца. Одна небольшая ошибка; размер очереди должен быть инициализирован равным n + 1, поскольку это максимальный размер очереди.
Эрик Липперт,
ReSharper не понимает ваш код ( присваивание в условном выражении ), но мне он вроде как нравится +1
Грозный
44

В качестве альтернативы созданию собственного метода и в случае, если порядок элементов не важен, будет работать следующий:

var result = sequence.Reverse().Skip(1);
Kamarey
источник
49
Обратите внимание, что для этого требуется, чтобы у вас было достаточно памяти для хранения всей последовательности, и, конечно же, он ВСЕ ЕЩЕ дважды повторяет всю последовательность: один раз для построения обратной последовательности и один раз для ее повторения. В значительной степени это хуже, чем решение Count, как бы вы его ни нарезали; он медленнее И занимает гораздо больше памяти.
Эрик Липперт,
Я не знаю, как работает метод «Обратный», поэтому не уверен, сколько памяти он использует. Но я согласен насчет двух итераций. Этот метод не следует использовать для больших коллекций или если важна производительность.
Камарей
5
А как бы вы реализовали Reverse? Можете ли вы в целом придумать способ сделать это без сохранения всей последовательности?
Эрик Липперт,
2
Мне это нравится, и он соответствует критериям не генерировать последовательность дважды.
Amy B
12
и, кроме того, вам также нужно будет снова перевернуть всю последовательность, чтобы сохранить ее, поскольку это equence.Reverse().Skip(1).Reverse()не очень хорошее решение
шашват
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;
    }
}

Согласно предложению Эрика Липперта, его легко обобщить на 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случай не требует специальной обработки.

Joren
источник
В первом примере, вероятно, было бы немного быстрее установить buffered=falseв предложении else перед назначением buffer. В любом случае условие уже проверяется, но это позволит избежать повторной установки bufferedкаждый раз в цикле.
Джеймс
Может кто-нибудь сказать мне плюсы / минусы этого по сравнению с выбранным ответом?
Sinjai 08
В чем преимущество реализации в отдельном методе, в котором отсутствуют проверки ввода? Кроме того, я бы просто отказался от единственной реализации и дал множественной реализации значение по умолчанию.
jpmc26
@ jpmc26 При проверке отдельным методом вы фактически получаете подтверждение в момент вызова DropLast. В противном случае проверка происходит только тогда, когда вы фактически перечисляете последовательность (при первом вызове MoveNextполученногоIEnumerator ). Неинтересно отлаживать, когда между генерацией IEnumerableи фактическим перечислением может быть произвольный объем кода . В настоящее время я бы написал InternalDropLastкак внутреннюю функцию DropLast, но этой функции не было в C #, когда я писал этот код 9 лет назад.
Joren
28

Enumerable.SkipLast(IEnumerable<TSource>, Int32)Метод был добавлен в .NET 2.1 стандарта. Он делает именно то, что вы хотите.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Возвращает новую перечисляемую коллекцию, которая содержит элементы из источника с пропущенными последними элементами подсчета исходной коллекции.

Джастин Лессард
источник
2
Это также есть в MoreLinq
Leperkawn
+1 для SkipLast. Я не знал этого, так как недавно перешел из .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из if.
Caleb
Этот код выглядит не так. Логика, кажется, такая: «вернуть неинициализированное prevв первой итерации, а после этого цикл навсегда».
Фил Миллер
Код, кажется, имеет ошибку "времени компиляции". Может быть, вы захотите это исправить. Но да, мы можем написать расширитель, который выполняет итерацию один раз и возвращает все, кроме последнего элемента.
Маниш Басантани,
@Caleb: Вы совершенно правы - я написал это в спешке! Исправлено сейчас. @Amby: Эээ, не знаю, какой компилятор вы используете. Ничего подобного у меня не было. : P
Нолдорин
@RobertSchmidt Упс, да. Я добавил usingзаявление сейчас.
нолдорин
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);
}

Где shrink реализует простой счетчик вперед для отбрасывания первых leftэлементов и тот же отброшенный буфер для отбрасывания последних rightэлементов.

silasdavis
источник
3

Если у вас нет времени на развертывание собственного расширения, вот более быстрый способ:

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });
SmallBizGuy
источник
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);
        }
    }
jr76
источник
2

Если вы можете получить Countили 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

Почему бы не просто .ToList<type>()в последовательности, а затем подсчитать вызовы и взять, как вы это делали изначально ... но поскольку он был помещен в список, он не должен выполнять дорогостоящее перечисление дважды. Правильно?

Брэди Мориц
источник
1

Решение, которое я использую для этой проблемы, немного сложнее.

Мой статический класс util содержит метод расширения, MarkEndкоторый преобразует T-items в EndMarkedItem<T>-items. Каждый элемент отмечен дополнительным значением int, равным либо 0 ; или (если вас особенно интересуют последние 3 пункта) -3 , -2 или -1 для последних 3 элементов.

Это может быть полезно само по себе, например, когда вы хотите создать список в простом foreachцикле с запятыми после каждого элемента, кроме последних 2, с предпоследним элементом, за которым следует слово-союз (например, « и » или « или »), и последний элемент, за которым следует точка.

Для создания всего списка без последних n элементов метод расширения ButLastпросто выполняет итерацию по EndMarkedItem<T>s while 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;
                }
            }
        }
    }
ddur
источник
1

Я не думаю, что это может быть более лаконично, чем это - также гарантируя удаление 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;
            }
        }
    }
}

Изменить: технически идентично этому ответу .

Роберт Шмидт
источник
1

В C # 8.0 для этого можно использовать диапазоны и индексы .

var allButLast = sequence[..^1];

По умолчанию для C # 8.0 требуется .NET Core 3.0 или .NET Standard 2.1 (или выше). Отметьте этот поток, чтобы использовать его со старыми реализациями.

Эмилиано Руис
источник
0

Вы могли бы написать:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);
RoJaIt
источник
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 может быть традиционным, но сломан. Первый возвращаемый элемент - это всегда неопределенная буква U - даже если у «моделей» 1 элемент. В последнем случае я ожидал бы пустой результат.
Роберт Шмидт
Кроме того, IEnumerator <T> является IDisposable.
Роберт Шмидт
True / отмечено. Спасибо.
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());

Думаю, это должно быть как "Где", но с сохранением порядка (?).

Гильермо Арес
источник
3
Это очень неэффективный способ сделать это. Чтобы оценить sequence.Last (), он должен пройти всю последовательность, делая это для каждого элемента в последовательности. O (n ^ 2) эффективность.
Майк
Ты прав. Не знаю, о чем я думал, когда писал этот XD. В любом случае, вы уверены, что Last () пройдёт всю последовательность? Для некоторых реализаций IEnumerable (например, Array) это должно быть O (1). Я не проверял реализацию List, но у нее также может быть «обратный» итератор, начинающийся с последнего элемента, который тоже принимает O (1). Кроме того, вы должны принять во внимание, что O (n) = O (2n), по крайней мере, технически говоря. Следовательно, если эта процедура не является абсолютно критичной для производительности ваших приложений, я бы придерживался гораздо более четкой последовательности: Take (sequence.Count () - 1). С уважением!
Гильермо Арес
@Mike Я не согласен с вами, приятель, sequence.Last () - это O (1), поэтому ему не нужно проходить всю последовательность. stackoverflow.com/a/1377895/812598
GoRoS
1
@GoRoS, это только O (1), если последовательность реализует ICollection / IList или является массивом. Все остальные последовательности - O (N). В своем вопросе я не предполагаю, что один из источников O (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];
}

Для этого требуется, чтобы последовательность была массивом, поскольку она имеет фиксированную длину и индексированные элементы.

einord
источник
2
Вы имеете дело с IEnumerable, который не разрешает доступ к элементам через индекс. Ваше решение не работает. Предполагая, что вы все делаете правильно, потребуется пройти по последовательности, чтобы определить длину, выделить массив длины n-1 и скопировать все элементы. - 1. 2n-1 операции и (2n-1) * (4 или 8 байт) памяти. Это даже не быстро.
Tarik
-4

Я бы, наверное, сделал что-то вроде этого:

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

Это одна итерация с проверкой, что она не последняя для каждого раза.

einord
источник
3
Две причины, по которым это нехорошо. 1) .LastOrDefault () требует повторения всей последовательности, и это вызывается для каждого элемента в последовательности (в .Where ()). 2) Если последовательность [1,2,1,2,1,2] и вы использовали свою технику, у вас останется [1,1,1].
Майк