Як взяти всі, крім останнього, елементи в послідовності за допомогою 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) алгоритмом ефективності простору, де останній також повинен переміщувати елементи у внутрішньому масиві.
Дікам

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

7
Тепер ви можете узагальнити це, щоб взяти все, крім остаточного n?
Ерік Ліпперт

4
Приємно. Одна невелика помилка; Розмір черги слід ініціалізувати до n + 1, оскільки це максимальний розмір черги.
Ерік Ліпперт

ReSharper не розуміє ваш код ( призначення в умовному вираженні ), але мені якось подобається +1
Грозный

44

Як альтернатива створенню власного методу, і якщо порядок елементів не є важливим, наступне буде працювати:

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

49
Зауважте, що для цього потрібно мати достатню кількість пам'яті для зберігання всієї послідовності, і звичайно вона СТАЛЬНО повторює всю послідовність двічі, один раз для складання перевернутої послідовності та одного разу для її повторення. Насправді це гірше, ніж рішення Count, незалежно від того, як ви його нарізаєте; це повільніше І займає набагато більше пам’яті.
Ерік Ліпперт

Я не знаю, як працює метод "Зворотний", тому я не впевнений у кількості пам'яті, яку він використовує. Але я погоджуюсь щодо двох ітерацій. Цей метод не слід застосовувати у великих колекціях або якщо важлива продуктивність.
Камарей

5
Ну, як би ви реалізували Реверс? Чи можете ви взагалі придумати спосіб зробити це, не зберігаючи всю послідовність?
Ерік Ліпперт

2
Мені це подобається, і він відповідає критеріям не генерування послідовності двічі.
Емі Б

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

Згідно з пропозицією Еріка Ліпперта, він легко узагальнюється до 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в іншому пункті перед призначенням buffer. Умова все-таки перевіряється, але це дозволить уникнути зайвого встановлення bufferedкожного разу через цикл.
Джеймс

Чи може хтось сказати мені плюси та мінуси цього проти обраної відповіді?
Сінджай

Яка перевага, якщо реалізація є окремим методом, який не має вхідних перевірок? Крім того, я б просто відкинув одну реалізацію і призначив для декількох реалізацій значення за замовчуванням.
jpmc26

@ jpmc26 Перевіряючи окремим методом, ви фактично отримуєте перевірку в момент, коли ви телефонуєте DropLast. Інакше перевірка відбувається лише тоді, коли ви фактично перераховуєте послідовність (під час першого дзвінка до MoveNextрезультату)IEnumerator ). Не приємно налагоджувати, коли між генеруванням IEnumerableта фактичним його перерахуванням може бути довільна кількість коду . Сьогодні я б писав InternalDropLastяк внутрішню функцію DropLast, але ця функціональність не існувала в C #, коли я писав цей код 9 років тому.
Жорен

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.
Калеб

Цей код не працює. Логіка, здається, "поверне неініціалізовану 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багато елементів.


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

Якщо ви можете отримати 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, і ваше рішення полягає в тому, чого ОП намагається уникнути. Ваш код має вплив на ефективність.
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>sEndMark == 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

Я не думаю, що це може стати більш лаконічним, ніж це - також забезпечуючи розпорядження 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 може бути традиційним, але порушений. Перший елемент, який він повертає, завжди є невизначеним U - навіть коли "моделі" мають 1 елемент. В останньому випадку я очікую порожнього результату.
Роберт Шмідт

Крім того, IEnumerator <T> є ідентифікаційним.
Роберт Шмідт

Правда / відмічено. Дякую.
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, який використовується для прийняття відомої кількості предметів. А черга на досить велику кількість - жахлива.
Синатр

-2

Може бути:

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

Я думаю, це має бути як де "Де", але зберігаючи порядок (?).


3
Це дуже неефективний спосіб зробити це. Для оцінки послідовності.Last () він повинен пройти всю послідовність, роблячи це для кожного елемента в послідовності. O (n ^ 2) ефективність.
Майк

Ти правий. Я не знаю, про що я думав, коли писав цей XD. У будь-якому випадку, ви впевнені, що Last () пройде всю послідовність? Для деяких реалізацій IEnumerable (наприклад, Array) це повинно бути O (1). Я не перевірив реалізацію списку, але він також міг би мати "зворотний" ітератор, починаючи з останнього елемента, який також займе O (1). Також слід врахувати, що O (n) = O (2n), принаймні технічно кажучи. Отже, якщо ця процедура не є абсолютно критичною для роботи ваших додатків, я б дотримувався набагато чіткішої послідовності. Взяти (послідовність.Count () - 1). З повагою!
Гільєрмо Арес

@Mike Я не згоден з вами товариш, послідовність. Остання () - це O (1), так що їй не потрібно проходити всю послідовність. stackoverflow.com/a/1377895/812598
GoRoS

1
@GoRoS, це лише O (1), якщо послідовність реалізує ICollection / IList або це масив. Всі інші послідовності є O (N). У своєму питанні я не припускаю, що одне з джерел O (1)
Майк

3
У послідовності може бути декілька елементів, що задовольняють цій умові e == послідовність. Останнє (), наприклад 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 байт) пам'яті. Це навіть не швидко.
Тарік

-4

Я, певно, зробив щось подібне:

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

Це одна ітерація з перевіркою, що вона не є останньою для кожного разу.


3
Дві причини - це не дуже добре. 1) .LastOrDefault () вимагає повторення всієї послідовності, і це викликається для кожного елемента в послідовності (у .Where ()). 2) Якщо послідовність становить [1,2,1,2,1,2] і ви використовували свою техніку, вам залишиться [1,1,1].
Майк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.