Đưa ra một bộ sưu tập, có cách nào để có được các yếu tố N cuối cùng của bộ sưu tập đó không? Nếu không có một phương thức nào trong khung, thì cách tốt nhất để viết một phương thức mở rộng là gì?
Đưa ra một bộ sưu tập, có cách nào để có được các yếu tố N cuối cùng của bộ sưu tập đó không? Nếu không có một phương thức nào trong khung, thì cách tốt nhất để viết một phương thức mở rộng là gì?
Câu trả lời:
collection.Skip(Math.Max(0, collection.Count() - N));
Cách tiếp cận này bảo tồn thứ tự vật phẩm mà không phụ thuộc vào bất kỳ sự sắp xếp nào và có khả năng tương thích rộng trên một số nhà cung cấp LINQ.
Điều quan trọng là phải cẩn thận không gọi Skip
với số âm. Một số nhà cung cấp, chẳng hạn như Entity Framework, sẽ tạo ra một ArgumentException khi được trình bày với một đối số phủ định. Các cuộc gọi để Math.Max
tránh điều này gọn gàng.
Lớp bên dưới có tất cả các yếu tố cần thiết cho các phương thức mở rộng, đó là: một lớp tĩnh, một phương thức tĩnh và sử dụng this
từ khóa.
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));
}
}
Một lưu ý ngắn gọn về hiệu suất:
Vì lệnh gọi Count()
có thể gây ra sự liệt kê của một số cấu trúc dữ liệu nhất định, phương pháp này có nguy cơ gây ra hai lần truyền dữ liệu. Đây thực sự không phải là một vấn đề với hầu hết các liệt kê; trong thực tế, tối ưu hóa đã tồn tại cho các truy vấn Danh sách, Mảng và thậm chí cả EF để đánh giá Count()
hoạt động trong thời gian O (1).
Tuy nhiên, nếu bạn phải sử dụng một số đếm chỉ chuyển tiếp và muốn tránh thực hiện hai đường chuyền, hãy xem xét thuật toán một lượt như Lasse V. Karlsen hoặc Mark Byers mô tả. Cả hai cách tiếp cận này đều sử dụng bộ đệm tạm thời để giữ các vật phẩm trong khi liệt kê, được mang lại khi kết thúc bộ sưu tập được tìm thấy.
List
s và LinkedList
s, giải pháp của James có xu hướng nhanh hơn, mặc dù không phải theo một độ lớn. Nếu IEnumerable được tính toán (thông qua Enumerable.Range, ví dụ), giải pháp của James sẽ mất nhiều thời gian hơn. Tôi không thể nghĩ ra bất kỳ cách nào để đảm bảo một lượt đi mà không biết gì về việc triển khai hoặc sao chép các giá trị vào một cấu trúc dữ liệu khác.
coll.Reverse().Take(N).Reverse().ToList();
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
return coll.Reverse().Take(N).Reverse();
}
CẬP NHẬT: Để giải quyết vấn đề của clintp: a) Sử dụng phương thức TakeLast () mà tôi đã xác định ở trên sẽ giải quyết vấn đề, nhưng nếu bạn thực sự muốn làm điều đó mà không có phương thức bổ sung, thì bạn chỉ cần nhận ra rằng trong khi Enumerable.Reverse () có thể được sử dụng như một phương thức mở rộng, bạn không bắt buộc phải sử dụng theo cách đó:
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();
Tôi gặp lỗi trình biên dịch vì .Reverse () trả về void và trình biên dịch chọn phương thức đó thay vì Linq trả về IEnumerable. Gợi ý?
N
hồ sơ cuối cùng, bạn có thể bỏ qua lần thứ hai Reverse
.
Lưu ý : Tôi đã bỏ lỡ tiêu đề câu hỏi của bạn có sử dụng Linq , vì vậy trên thực tế câu trả lời của tôi không sử dụng Linq.
Nếu bạn muốn tránh lưu vào bộ đệm một bản sao không lười biếng của toàn bộ bộ sưu tập, bạn có thể viết một phương thức đơn giản sử dụng danh sách được liên kết.
Phương pháp sau đây sẽ thêm từng giá trị mà nó tìm thấy trong bộ sưu tập gốc vào danh sách được liên kết và cắt danh sách được liên kết xuống theo số lượng mục cần thiết. Vì nó giữ cho danh sách được liên kết được cắt tỉa theo số lượng mục này trong toàn bộ thời gian thông qua việc lặp qua bộ sưu tập, nên nó sẽ chỉ giữ một bản sao của hầu hết N mục từ bộ sưu tập ban đầu.
Nó không yêu cầu bạn phải biết số lượng vật phẩm trong bộ sưu tập ban đầu, cũng không lặp đi lặp lại nhiều lần.
Sử dụng:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
Phương pháp mở rộng:
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;
}
}
Đây là một phương thức hoạt động trên mọi liệt kê nhưng chỉ sử dụng bộ lưu trữ tạm thời 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];
}
}
}
Sử dụng:
List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();
Nó hoạt động bằng cách sử dụng bộ đệm vòng có kích thước N để lưu trữ các phần tử khi nhìn thấy chúng, ghi đè các phần tử cũ bằng phần tử mới. Khi kết thúc vô số, bộ đệm vòng chứa N phần tử cuối cùng.
n
.
.NET Core 2.0+ cung cấp phương thức LINQ TakeLast()
:
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast
ví dụ :
Enumerable
.Range(1, 10)
.TakeLast(3) // <--- takes last 3 items
.ToList()
.ForEach(i => System.Console.WriteLine(i))
// outputs:
// 8
// 9
// 10
netcoreapp1.x
) mà chỉ dành cho v2.0 & v2.1 của dotnetcore ( netcoreapp2.x
). Có thể bạn có thể nhắm mục tiêu khung đầy đủ (ví dụ net472
) cũng không được hỗ trợ. (libs tiêu chuẩn .net có thể được sử dụng bởi bất kỳ điều nào ở trên nhưng chỉ có thể hiển thị một số API nhất định cụ thể cho khung mục tiêu. xem docs.microsoft.com/en-us/dotnet/stiteria/frameworks )
Tôi ngạc nhiên khi không ai nhắc đến nó, nhưng SkipWhile có một phương pháp sử dụng chỉ mục của phần tử .
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)
Lợi ích duy nhất có thể nhận thấy mà giải pháp này mang lại cho người khác là bạn có thể có tùy chọn để thêm vào một vị từ để tạo một truy vấn LINQ mạnh mẽ và hiệu quả hơn, thay vì có hai thao tác riêng biệt đi qua IEnumerable hai lần.
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));
}
Sử dụng EnumerableEx.TakeLast trong lắp ráp System.Interactive của RX. Đó là một triển khai O (N) như @ Mark, nhưng nó sử dụng hàng đợi thay vì cấu trúc bộ đệm vòng (và loại bỏ các mục khi đạt đến dung lượng bộ đệm).
(NB: Đây là phiên bản IEnumerable - không phải phiên bản IObservable, mặc dù việc triển khai hai phiên bản này khá giống nhau)
Queue<T>
thực hiện bằng cách sử dụng bộ đệm tròn ?
Nếu bạn đang xử lý một bộ sưu tập bằng một khóa (ví dụ: các mục từ cơ sở dữ liệu), một giải pháp nhanh (tức là nhanh hơn câu trả lời đã chọn) sẽ là
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
Nếu bạn không ngại nhúng vào Rx như một phần của đơn nguyên, bạn có thể sử dụng TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
Nếu sử dụng thư viện của bên thứ ba là một tùy chọn, MoreLinq sẽ xác định TakeLast()
chính xác việc này.
Tôi đã cố gắng kết hợp hiệu quả và đơn giản và kết thúc với điều này:
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;
}
Về hiệu suất: Trong C #, Queue<T>
được triển khai bằng cách sử dụng bộ đệm tròn để không có việc khởi tạo đối tượng được thực hiện mỗi vòng lặp (chỉ khi hàng đợi tăng lên). Tôi không đặt dung lượng hàng đợi (sử dụng hàm tạo chuyên dụng) vì ai đó có thể gọi phần mở rộng này với count = int.MaxValue
. Để có hiệu suất cao hơn, bạn có thể kiểm tra xem nguồn có thực hiện IList<T>
hay không và nếu có, hãy trích xuất trực tiếp các giá trị cuối cùng bằng cách sử dụng các chỉ mục mảng.
Việc lấy N cuối cùng của bộ sưu tập bằng LINQ là không hiệu quả vì tất cả các giải pháp trên đều yêu cầu lặp lại trên toàn bộ sưu tập. TakeLast(int n)
trong System.Interactive
cũng có vấn đề này.
Nếu bạn có một danh sách, một việc hiệu quả hơn là cắt nó bằng phương pháp sau
/// 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);
}
với
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;
}
và một số trường hợp thử nghiệm
[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});
}
Tôi biết đã muộn để trả lời câu hỏi này. Nhưng nếu bạn đang làm việc với bộ sưu tập loại IList <> và bạn không quan tâm đến thứ tự của bộ sưu tập được trả về, thì phương pháp này hoạt động nhanh hơn. Tôi đã sử dụng câu trả lời của Mark Byers và thực hiện một số thay đổi nhỏ. Vì vậy, bây giờ phương thức TakeLast là:
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];
}
}
}
Để thử nghiệm, tôi đã sử dụng phương pháp Mark Byers và kbrimington's andswer . Đây là bài kiểm tra:
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();
Và đây là kết quả để lấy 10 yếu tố:
và để lấy 1000001 yếu tố kết quả là:
Đây là giải pháp của tôi:
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();
}
}
}
}
Mã này hơi chunky, nhưng là một thành phần có thể tái sử dụng, nó sẽ hoạt động tốt như trong hầu hết các kịch bản, và nó sẽ giữ cho mã sử dụng nó tốt và ngắn gọn. :-)
Của tôi TakeLast
khôngIList`1
dựa trên thuật toán đệm vòng giống như trong các câu trả lời của @Mark Byers và @MackieChan hơn nữa. Thật thú vị khi chúng giống nhau - Tôi đã viết của tôi hoàn toàn độc lập. Đoán thực sự chỉ có một cách để thực hiện bộ đệm vòng đúng cách. :-)
Nhìn vào câu trả lời của @ kbrimington, một kiểm tra bổ sung có thể được thêm vào để kiểm tra IQuerable<T>
lại cách tiếp cận hoạt động tốt với Entity Framework - giả sử rằng những gì tôi có tại thời điểm này không có.
Bên dưới ví dụ thực tế làm thế nào để lấy 3 phần tử cuối cùng từ một bộ sưu tập (mảng):
// 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();
Sử dụng phương pháp này để có được tất cả phạm vi mà không có lỗi
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 ;
}
Ít thực hiện khác nhau với việc sử dụng bộ đệm tròn. Các điểm chuẩn cho thấy phương thức này nhanh hơn hai lần so với phương pháp sử dụng Hàng đợi (triển khai TakeLast trong System.Linq ), tuy nhiên không phải không có chi phí - nó cần một bộ đệm phát triển cùng với số lượng phần tử được yêu cầu, ngay cả khi bạn có bộ sưu tập nhỏ bạn có thể nhận được phân bổ bộ nhớ lớn.
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];
}
}