답변:
collection.Skip(Math.Max(0, collection.Count() - N));
이 접근 방식은 정렬에 의존하지 않고 항목 순서를 유지하며 여러 LINQ 공급자간에 광범위하게 호환됩니다.
Skip
음수 로 전화하지 않도록주의해야합니다 . Entity Framework와 같은 일부 공급자는 부정적인 인수가 표시되면 ArgumentException을 생성합니다. Math.Max
깔끔하게 피하 라는 부름 .
아래 클래스에는 확장 메소드에 대한 모든 필수 사항이 있습니다. 정적 메소드, 정적 메소드 및 this
키워드 사용입니다 .
public static class MiscExtensions
{
// Ex: collection.TakeLast(5);
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
{
return source.Skip(Math.Max(0, source.Count() - N));
}
}
성능에 대한 간단한 참고 사항 :
호출하면 Count()
특정 데이터 구조가 열거 될 수 있으므로이 접근 방식은 데이터를 두 번 통과시킬 위험이 있습니다. 이것은 실제로 대부분의 열거 가능한 문제가 아닙니다. 실제로 Count()
O (1) 시간 내에 연산 을 평가하기 위해 목록, 배열 및 EF 쿼리에 대한 최적화가 이미 존재합니다 .
그러나 순방향 전용 열거 형을 사용해야하고 두 번의 통과를 피하려면 Lasse V. Karlsen 또는 Mark Byers 와 같은 단일 패스 알고리즘을 고려하십시오 . 이 두 가지 접근 방법은 열거하는 동안 항목을 보유하기 위해 임시 버퍼를 사용하며, 콜렉션의 끝이 발견되면 생성됩니다.
List
s 및 LinkedList
s를 사용하면 수십 배는 아니지만 더 빠른 경향이 있습니다. IEnumerable이 (Enumerable.Range 등을 통해) 계산되면 James의 솔루션이 더 오래 걸립니다. 구현에 대해 알지 못하거나 다른 데이터 구조에 값을 복사하지 않고 단일 패스를 보장하는 방법을 생각할 수 없습니다.
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
업데이트 : clintp의 문제를 해결하려면 : a) 위에서 정의한 TakeLast () 메소드를 사용하면 문제가 해결되지만 추가 메소드없이 실제로 수행하려면 Enumerable.Reverse ()가 될 수 있음을 인식해야합니다. 확장 방법으로 사용되면 다음과 같이 사용할 필요가 없습니다.
List<string> mystring = new List<string>() { "one", "two", "three" };
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();
List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse();
.Reverse ()를 무효 반환 및 컴파일러이 선택하는 대신는 IEnumerable을 반환 Linq에 하나의 방법 때문에 나는 컴파일러 오류가 발생합니다. 제안?
N
레코드를 얻은 후 주문에 신경 쓰지 않으면 두 번째 레코드를 건너 뛸 수 있습니다 Reverse
.
참고 : Linq 사용 이라는 질문 제목을 놓쳤 으므로 실제로 Linq를 사용하지 않습니다.
전체 컬렉션의 지연되지 않은 복사본을 캐싱하지 않으려면 연결된 목록을 사용하여 간단한 방법을 작성할 수 있습니다.
다음 방법은 원본 컬렉션에서 찾은 각 값을 연결 목록에 추가하고 연결 목록을 필요한 항목 수로 줄입니다. 컬렉션을 반복하는 동안 링크 된 목록이이 항목 수만큼 잘린 상태로 유지되므로 원본 컬렉션에서 최대 N 개의 항목 복사본 만 유지합니다.
원본 컬렉션의 항목 수를 알 필요가 없으며 여러 번 반복하지 않아도됩니다.
용법:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
확장 방법 :
public static class Extensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection,
int n)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), $"{nameof(n)} must be 0 or greater");
LinkedList<T> temp = new LinkedList<T>();
foreach (var value in collection)
{
temp.AddLast(value);
if (temp.Count > n)
temp.RemoveFirst();
}
return temp;
}
}
열거 가능한 모든 작업에서 작동하지만 O (N) 임시 저장소 만 사용하는 방법은 다음과 같습니다.
public static class TakeLastExtension
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int takeCount)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
if (takeCount == 0) { yield break; }
T[] result = new T[takeCount];
int i = 0;
int sourceCount = 0;
foreach (T element in source)
{
result[i] = element;
i = (i + 1) % takeCount;
sourceCount++;
}
if (sourceCount < takeCount)
{
takeCount = sourceCount;
i = 0;
}
for (int j = 0; j < takeCount; ++j)
{
yield return result[(i + j) % takeCount];
}
}
}
용법:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
N 크기의 링 버퍼를 사용하여 요소를 볼 때 저장하여 이전 요소를 새 요소로 덮어 씁니다. 열거 가능 항목의 끝에 도달하면 링 버퍼에 마지막 N 개의 요소가 포함됩니다.
n
.
.NET Core 2.0+는 LINQ 방법을 제공합니다 TakeLast()
.
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
예 :
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
) 에는 사용할 수 없지만 dotnetcore ( netcoreapp2.x
) 의 v2.0 및 v2.1에만 사용할 수 있습니다 . net472
지원되지 않는 전체 프레임 워크 (예 :)를 타겟팅 할 수도 있습니다. (.net 표준 라이브러리는 위의 방법으로 사용할 수 있지만 대상 프레임 워크에 특정한 특정 API 만 노출 할 수 있습니다. docs.microsoft.com/en-us/dotnet/standard/frameworks 참조 )
아무도 언급하지 않았지만 SkipWhile에는 요소의 index 를 사용 하는 메소드가 있습니다 .
public static IEnumerable<T> TakeLastN<T>(this IEnumerable<T> source, int n)
{
if (source == null)
throw new ArgumentNullException("Source cannot be null");
int goldenIndex = source.Count() - n;
return source.SkipWhile((val, index) => index < goldenIndex);
}
//Or if you like them one-liners (in the spirit of the current accepted answer);
//However, this is most likely impractical due to the repeated calculations
collection.SkipWhile((val, index) => index < collection.Count() - N)
이 솔루션이 다른 솔루션보다 유일하게 인식 할 수있는 이점은 IEnumerable을 두 번 통과하는 두 개의 별도 작업을 수행하는 대신보다 강력하고 효율적인 LINQ 쿼리를 만들기 위해 조건자를 추가 할 수 있다는 것입니다.
public static IEnumerable<T> FilterLastN<T>(this IEnumerable<T> source, int n, Predicate<T> pred)
{
int goldenIndex = source.Count() - n;
return source.SkipWhile((val, index) => index < goldenIndex && pred(val));
}
RX의 System.Interactive 어셈블리에서 EnumerableEx.TakeLast를 사용하십시오. @Mark와 같은 O (N) 구현이지만 링 버퍼 구문 대신 대기열을 사용합니다 (버퍼 용량에 도달하면 항목을 대기열에서 제외).
(NB : 이것은 IEnumerable 버전입니다. IObservable 버전은 아닙니다. 두 버전의 구현은 거의 동일합니다)
나는 효율성과 단순성을 결합하려고 노력했으며 이것으로 끝났습니다.
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
if (source == null) { throw new ArgumentNullException("source"); }
Queue<T> lastElements = new Queue<T>();
foreach (T element in source)
{
lastElements.Enqueue(element);
if (lastElements.Count > count)
{
lastElements.Dequeue();
}
}
return lastElements;
}
성능 정보 : C #에서는 순환 버퍼를Queue<T>
사용하여 구현 되므로 각 루프에서 수행되는 객체 인스턴스화가 없습니다 (큐가 커질 때만). 누군가이 확장을 호출 할 수 있기 때문에 (전용 생성자를 사용하여) 대기열 용량을 설정하지 않았습니다 . 추가 성능을 위해 소스 구현인지 확인 하고 예인 경우 배열 인덱스를 사용하여 마지막 값을 직접 추출하십시오.count = int.MaxValue
IList<T>
위의 모든 솔루션에서 컬렉션 전체를 반복해야하므로 LINQ를 사용하여 컬렉션의 마지막 N 개를 가져 오는 것은 약간 비효율적입니다. TakeLast(int n)
의는 System.Interactive
이 문제가 있습니다.
목록이 더 효율적인 경우 다음 방법을 사용하여 목록을 슬라이스하십시오.
/// Select from start to end exclusive of end using the same semantics
/// as python slice.
/// <param name="list"> the list to slice</param>
/// <param name="start">The starting index</param>
/// <param name="end">The ending index. The result does not include this index</param>
public static List<T> Slice<T>
(this IReadOnlyList<T> list, int start, int? end = null)
{
if (end == null)
{
end = list.Count();
}
if (start < 0)
{
start = list.Count + start;
}
if (start >= 0 && end.Value > 0 && end.Value > start)
{
return list.GetRange(start, end.Value - start);
}
if (end < 0)
{
return list.GetRange(start, (list.Count() + end.Value) - start);
}
if (end == start)
{
return new List<T>();
}
throw new IndexOutOfRangeException(
"count = " + list.Count() +
" start = " + start +
" end = " + end);
}
와
public static List<T> GetRange<T>( this IReadOnlyList<T> list, int index, int count )
{
List<T> r = new List<T>(count);
for ( int i = 0; i < count; i++ )
{
int j=i + index;
if ( j >= list.Count )
{
break;
}
r.Add(list[j]);
}
return r;
}
일부 테스트 사례
[Fact]
public void GetRange()
{
IReadOnlyList<int> l = new List<int>() { 0, 10, 20, 30, 40, 50, 60 };
l
.GetRange(2, 3)
.ShouldAllBeEquivalentTo(new[] { 20, 30, 40 });
l
.GetRange(5, 10)
.ShouldAllBeEquivalentTo(new[] { 50, 60 });
}
[Fact]
void SliceMethodShouldWork()
{
var list = new List<int>() { 1, 3, 5, 7, 9, 11 };
list.Slice(1, 4).ShouldBeEquivalentTo(new[] { 3, 5, 7 });
list.Slice(1, -2).ShouldBeEquivalentTo(new[] { 3, 5, 7 });
list.Slice(1, null).ShouldBeEquivalentTo(new[] { 3, 5, 7, 9, 11 });
list.Slice(-2)
.Should()
.BeEquivalentTo(new[] {9, 11});
list.Slice(-2,-1 )
.Should()
.BeEquivalentTo(new[] {9});
}
이 질문에 답하기에는 늦었다는 것을 알고 있습니다. 그러나 IList <> 형식의 컬렉션을 사용하고 있고 반환 된 컬렉션의 순서를 신경 쓰지 않으면이 방법이 더 빨리 작동합니다. Mark Byers의 답변을 사용 하고 약간 변경했습니다. 이제 TakeLast 메소드는 다음과 같습니다.
public static IEnumerable<T> TakeLast<T>(IList<T> source, int takeCount)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
if (takeCount == 0) { yield break; }
if (source.Count > takeCount)
{
for (int z = source.Count - 1; takeCount > 0; z--)
{
takeCount--;
yield return source[z];
}
}
else
{
for(int i = 0; i < source.Count; i++)
{
yield return source[i];
}
}
}
테스트를 위해 Mark Byers 방법과 kbrimington 's andswer을 사용했습니다 . 이것은 테스트입니다.
IList<int> test = new List<int>();
for(int i = 0; i<1000000; i++)
{
test.Add(i);
}
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
IList<int> result = TakeLast(test, 10).ToList();
stopwatch.Stop();
Stopwatch stopwatch1 = new Stopwatch();
stopwatch1.Start();
IList<int> result1 = TakeLast2(test, 10).ToList();
stopwatch1.Stop();
Stopwatch stopwatch2 = new Stopwatch();
stopwatch2.Start();
IList<int> result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList();
stopwatch2.Stop();
다음은 10 가지 요소를 사용한 결과입니다.
그리고 1000001 요소를 취한 결과는 다음과 같습니다.
내 해결책은 다음과 같습니다.
public static class EnumerationExtensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int count)
{
if (count <= 0)
yield break;
var inputList = input as IList<T>;
if (inputList != null)
{
int last = inputList.Count;
int first = last - count;
if (first < 0)
first = 0;
for (int i = first; i < last; i++)
yield return inputList[i];
}
else
{
// Use a ring buffer. We have to enumerate the input, and we don't know in advance how many elements it will contain.
T[] buffer = new T[count];
int index = 0;
count = 0;
foreach (T item in input)
{
buffer[index] = item;
index = (index + 1) % buffer.Length;
count++;
}
// The index variable now points at the next buffer entry that would be filled. If the buffer isn't completely
// full, then there are 'count' elements preceding index. If the buffer *is* full, then index is pointing at
// the oldest entry, which is the first one to return.
//
// If the buffer isn't full, which means that the enumeration has fewer than 'count' elements, we'll fix up
// 'index' to point at the first entry to return. That's easy to do; if the buffer isn't full, then the oldest
// entry is the first one. :-)
//
// We'll also set 'count' to the number of elements to be returned. It only needs adjustment if we've wrapped
// past the end of the buffer and have enumerated more than the original count value.
if (count < buffer.Length)
index = 0;
else
count = buffer.Length;
// Return the values in the correct order.
while (count > 0)
{
yield return buffer[index];
index = (index + 1) % buffer.Length;
count--;
}
}
}
public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> input, int count)
{
if (count <= 0)
return input;
else
return input.SkipLastIter(count);
}
private static IEnumerable<T> SkipLastIter<T>(this IEnumerable<T> input, int count)
{
var inputList = input as IList<T>;
if (inputList != null)
{
int first = 0;
int last = inputList.Count - count;
if (last < 0)
last = 0;
for (int i = first; i < last; i++)
yield return inputList[i];
}
else
{
// Aim to leave 'count' items in the queue. If the input has fewer than 'count'
// items, then the queue won't ever fill and we return nothing.
Queue<T> elements = new Queue<T>();
foreach (T item in input)
{
elements.Enqueue(item);
if (elements.Count > count)
yield return elements.Dequeue();
}
}
}
}
코드는 약간 엉성하지만 재사용이 가능한 드롭 인 구성 요소로 대부분의 시나리오에서 가능한 한 잘 수행해야하며 코드를 사용하는 코드를 간결하고 간결하게 유지합니다. :-)
My TakeLast
for non- IList`1
은 @Mark Byers 및 @MackieChan의 답변과 동일한 링 버퍼 알고리즘을 기반으로합니다. 그것들이 얼마나 비슷한 지 흥미 롭습니다. 저는 완전히 독립적으로 글을 썼습니다. 링 버퍼를 올바르게 수행하는 유일한 방법이 있다고 생각합니다. :-)
@kbrimington의 답변을 살펴보면 IQuerable<T>
Entity Framework와 잘 작동하는 접근법으로 돌아 가기 위해 추가 검사를 추가 할 수 있습니다 .이 시점에서 내가 가지고있는 것이 그렇지 않다고 가정합니다.
실제 예제 아래에서 컬렉션 (어레이)에서 마지막 3 개 요소를 가져 오는 방법 :
// split address by spaces into array
string[] adrParts = adr.Split(new string[] { " " },StringSplitOptions.RemoveEmptyEntries);
// take only 3 last items in array
adrParts = adrParts.SkipWhile((value, index) => { return adrParts.Length - index > 3; }).ToArray();
이 방법을 사용하여 오류없이 모든 범위 가져 오기
public List<T> GetTsRate( List<T> AllT,int Index,int Count)
{
List<T> Ts = null;
try
{
Ts = AllT.ToList().GetRange(Index, Count);
}
catch (Exception ex)
{
Ts = AllT.Skip(Index).ToList();
}
return Ts ;
}
순환 버퍼 사용으로 거의 다른 구현. 벤치 마크에 따르면이 방법은 Queue ( System.Linq 에서 TakeLast 구현)를 사용하는 방법보다 2 배 빠릅니다 . 비용이 들지 않습니다. 작은 컬렉션은 거대한 메모리 할당을 얻을 수 있습니다.
public IEnumerable<T> TakeLast<T>(IEnumerable<T> source, int count)
{
int i = 0;
if (count < 1)
yield break;
if (source is IList<T> listSource)
{
if (listSource.Count < 1)
yield break;
for (i = listSource.Count < count ? 0 : listSource.Count - count; i < listSource.Count; i++)
yield return listSource[i];
}
else
{
bool move = true;
bool filled = false;
T[] result = new T[count];
using (var enumerator = source.GetEnumerator())
while (move)
{
for (i = 0; (move = enumerator.MoveNext()) && i < count; i++)
result[i] = enumerator.Current;
filled |= move;
}
if (filled)
for (int j = i; j < count; j++)
yield return result[j];
for (int j = 0; j < i; j++)
yield return result[j];
}
}