Khi nào thì tốt hơn để sử dụng Danh sách so với LinkedList ?
Khi nào thì tốt hơn để sử dụng Danh sách so với LinkedList ?
Câu trả lời:
Xin vui lòng đọc các ý kiến cho câu trả lời này. Mọi người tuyên bố tôi đã không làm các xét nghiệm thích hợp. Tôi đồng ý đây không phải là một câu trả lời được chấp nhận. Khi tôi đang học, tôi đã làm một số bài kiểm tra và cảm thấy muốn chia sẻ chúng.
Tôi tìm thấy kết quả thú vị:
// Temporary class to show the example
class Temp
{
public decimal A, B, C, D;
public Temp(decimal a, decimal b, decimal c, decimal d)
{
A = a; B = b; C = c; D = d;
}
}
LinkedList<Temp> list = new LinkedList<Temp>();
for (var i = 0; i < 12345678; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
List<Temp> list = new List<Temp>(); // 2.4 seconds
for (var i = 0; i < 12345678; i++)
{
var a = new Temp(i, i, i, i);
list.Add(a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
Ngay cả khi bạn chỉ truy cập dữ liệu về cơ bản thì nó vẫn chậm hơn nhiều !! Tôi nói không bao giờ sử dụng một LinkedList.
Dưới đây là một so sánh khác thực hiện rất nhiều phần chèn (chúng tôi dự định chèn một mục ở giữa danh sách)
LinkedList<Temp> list = new LinkedList<Temp>();
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
var curNode = list.First;
for (var k = 0; k < i/2; k++) // In order to insert a node at the middle of the list we need to find it
curNode = curNode.Next;
list.AddAfter(curNode, a); // Insert it after
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
List<Temp> list = new List<Temp>();
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.Insert(i / 2, a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
list.AddLast(new Temp(1,1,1,1));
var referenceNode = list.First;
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
list.AddBefore(referenceNode, a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
Vì vậy, chỉ khi bạn có kế hoạch chèn một vài mục và bạn cũng ở đâu đó có tham chiếu về nơi bạn dự định chèn mục đó thì hãy sử dụng danh sách được liên kết. Chỉ vì bạn phải chèn nhiều mục mà nó không làm cho nó nhanh hơn bởi vì việc tìm kiếm vị trí mà bạn muốn chèn sẽ mất thời gian.
list.AddLast(a);
trong hai ví dụ LinkedList cuối cùng? Tôi bắt đầu thực hiện nó một lần trước vòng lặp, như list.AddLast(new Temp(1,1,1,1));
trong danh sách LinkedList cuối cùng, nhưng có vẻ như (với tôi) giống như bạn đang thêm gấp đôi số đối tượng Temp trong các vòng lặp. (Và khi tôi tự kiểm tra lại bản thân bằng một ứng dụng thử nghiệm , chắc chắn, gấp đôi số lượng trong LinkedList.)
I say never use a linkedList.
là thiếu sót khi bài đăng sau của bạn tiết lộ. Bạn có thể muốn chỉnh sửa nó. 2) Bạn định thời gian là gì? Khởi tạo, bổ sung và liệt kê hoàn toàn trong một bước? Hầu hết, khởi tạo và liệt kê không phải là điều ppl lo lắng, đó là những bước một lần. Cụ thể thời gian chèn và bổ sung sẽ cho một ý tưởng tốt hơn. 3) Quan trọng nhất, bạn đang thêm nhiều hơn yêu cầu vào danh sách liên kết. Đây là một so sánh sai. Truyền bá ý tưởng sai về danh sách liên kết.
Trong hầu hết các trường hợp, List<T>
là hữu ích hơn. LinkedList<T>
sẽ có ít chi phí hơn khi thêm / xóa các mục ở giữa danh sách, trong khi List<T>
chỉ có thể thêm / xóa giá rẻ ở cuối danh sách.
LinkedList<T>
chỉ hiệu quả nhất nếu bạn đang truy cập dữ liệu tuần tự (về phía trước hoặc phía sau) - truy cập ngẫu nhiên tương đối tốn kém vì nó phải đi theo chuỗi mỗi lần (do đó tại sao nó không có bộ chỉ mục). Tuy nhiên, vì a List<T>
về cơ bản chỉ là một mảng (có trình bao bọc) nên truy cập ngẫu nhiên là ổn.
List<T>
cũng cung cấp rất nhiều phương pháp hỗ trợ - Find
, ToArray
, vv; tuy nhiên, những điều này cũng có sẵn LinkedList<T>
với .NET 3.5 / C # 3.0 thông qua các phương thức mở rộng - vì vậy đó không phải là một yếu tố.
List<T>
và T[]
sẽ thất bại vì quá chunky (tất cả một phiến), LinkedList<T>
sẽ than van vì quá chi tiết (phiến cho mỗi phần tử).
Suy nghĩ về một danh sách được liên kết như một danh sách có thể là một chút sai lệch. Nó giống như một chuỗi hơn. Trong thực tế, trong .NET, LinkedList<T>
thậm chí không thực hiện IList<T>
. Không có khái niệm thực sự về chỉ mục trong danh sách được liên kết, mặc dù có vẻ như có. Chắc chắn không có phương thức nào được cung cấp trên lớp chấp nhận các chỉ mục.
Danh sách liên kết có thể được liên kết đơn, hoặc liên kết đôi. Điều này đề cập đến việc mỗi phần tử trong chuỗi chỉ có một liên kết đến phần tiếp theo (liên kết đơn) hay cả hai phần tử trước / phần tiếp theo (liên kết đôi). LinkedList<T>
được liên kết đôi.
Trong nội bộ, List<T>
được hỗ trợ bởi một mảng. Điều này cung cấp một đại diện rất nhỏ gọn trong bộ nhớ. Ngược lại, LinkedList<T>
liên quan đến bộ nhớ bổ sung để lưu trữ các liên kết hai chiều giữa các yếu tố kế tiếp nhau. Vì vậy, dung lượng bộ nhớ của a LinkedList<T>
thường sẽ lớn hơn so với List<T>
(với cảnh báo List<T>
có thể có các phần tử mảng bên trong không được sử dụng để cải thiện hiệu suất trong các hoạt động chắp thêm.)
Chúng có các đặc tính hiệu suất khác nhau:
LinkedList<T>.AddLast(item)
thời gian không đổiList<T>.Add(item)
khấu hao thời gian không đổi, trường hợp xấu nhất tuyến tínhLinkedList<T>.AddFirst(item)
thời gian không đổiList<T>.Insert(0, item)
thời gian tuyến tínhLinkedList<T>.AddBefore(node, item)
thời gian không đổiLinkedList<T>.AddAfter(node, item)
thời gian không đổiList<T>.Insert(index, item)
thời gian tuyến tínhLinkedList<T>.Remove(item)
thời gian tuyến tínhLinkedList<T>.Remove(node)
thời gian không đổiList<T>.Remove(item)
thời gian tuyến tínhList<T>.RemoveAt(index)
thời gian tuyến tínhLinkedList<T>.Count
thời gian không đổiList<T>.Count
thời gian không đổiLinkedList<T>.Contains(item)
thời gian tuyến tínhList<T>.Contains(item)
thời gian tuyến tínhLinkedList<T>.Clear()
thời gian tuyến tínhList<T>.Clear()
thời gian tuyến tínhNhư bạn có thể thấy, chúng hầu hết tương đương. Trong thực tế, API củaLinkedList<T>
nó cồng kềnh hơn để sử dụng và chi tiết về nhu cầu nội bộ của nó tràn vào mã của bạn.
Tuy nhiên, nếu bạn cần thực hiện nhiều thao tác chèn / xóa từ trong danh sách, nó sẽ cung cấp thời gian liên tục. List<T>
cung cấp thời gian tuyến tính, vì các mục bổ sung trong danh sách phải được xáo trộn xung quanh sau khi chèn / xóa.
Danh sách liên kết cung cấp chèn hoặc xóa rất nhanh của một thành viên danh sách. Mỗi thành viên trong danh sách được liên kết chứa một con trỏ tới thành viên tiếp theo trong danh sách để chèn thành viên vào vị trí i:
Bất lợi cho danh sách liên kết là không thể truy cập ngẫu nhiên. Truy cập một thành viên yêu cầu duyệt qua danh sách cho đến khi tìm thấy thành viên mong muốn.
Câu trả lời trước của tôi không đủ chính xác. Thật sự nó rất kinh khủng: D Nhưng bây giờ tôi có thể đăng câu trả lời hữu ích và chính xác hơn nhiều.
Tôi đã làm một số xét nghiệm bổ sung. Bạn có thể tìm thấy nguồn của nó bằng liên kết sau và tự kiểm tra lại nó trên môi trường của bạn: https://github.com/ukushu/DataSt StructuresTestsAndOther.git
Kết quả ngắn:
Mảng cần sử dụng:
Danh sách cần sử dụng:
LinkedList cần sử dụng:
Thêm chi tiết:
LinkedList<T>
bên trong không phải là một danh sách trong .NET. Nó thậm chí không thực hiện IList<T>
. Và đó là lý do tại sao không có chỉ mục và phương pháp vắng mặt liên quan đến chỉ mục.
LinkedList<T>
là bộ sưu tập dựa trên nút con trỏ. Trong .NET, nó được thực hiện liên kết đôi. Điều này có nghĩa là các yếu tố trước / tiếp theo có liên kết đến yếu tố hiện tại. Và dữ liệu bị phân mảnh - các đối tượng danh sách khác nhau có thể được đặt ở các vị trí khác nhau của RAM. Ngoài ra, sẽ có nhiều bộ nhớ được sử dụng LinkedList<T>
hơn cho List<T>
hoặc Array.
List<T>
trong .Net là sự thay thế của Java ArrayList<T>
. Điều này có nghĩa rằng đây là mảng bao bọc. Vì vậy, nó được phân bổ trong bộ nhớ dưới dạng một khối dữ liệu liền kề. Nếu kích thước dữ liệu được phân bổ vượt quá 85000 byte, nó sẽ được chuyển sang Heap đối tượng lớn. Tùy thuộc vào kích thước, điều này có thể dẫn đến phân mảnh heap (một dạng rò rỉ bộ nhớ nhẹ). Nhưng đồng thời nếu kích thước <85000 byte - điều này cung cấp một biểu diễn truy cập rất nhỏ gọn và nhanh chóng trong bộ nhớ.
Khối liền kề đơn được ưu tiên cho hiệu suất truy cập ngẫu nhiên và mức tiêu thụ bộ nhớ nhưng đối với các bộ sưu tập cần thay đổi kích thước thường xuyên, một cấu trúc như một mảng thường cần được sao chép sang một vị trí mới trong khi danh sách được liên kết chỉ cần quản lý bộ nhớ cho bộ nhớ mới được chèn / các nút bị xóa.
Sự khác biệt giữa List và LinkedList nằm ở cách triển khai cơ bản của chúng. Danh sách là tập hợp dựa trên mảng (ArrayList). LinkedList là bộ sưu tập dựa trên con trỏ nút (LinkedListNode). Về mức độ sử dụng API, cả hai đều giống nhau vì cả hai đều thực hiện cùng một bộ giao diện như ICollection, IEnumerable, v.v.
Sự khác biệt chính đến khi hiệu suất quan trọng. Ví dụ: nếu bạn đang triển khai danh sách có hoạt động "INSERT" nặng nề, LinkedList vượt trội hơn Danh sách. Vì LinkedList có thể làm điều đó trong thời gian O (1), nhưng List có thể cần mở rộng kích thước của mảng bên dưới. Để biết thêm thông tin / chi tiết, bạn có thể muốn đọc về sự khác biệt về thuật toán giữa LinkedList và cấu trúc dữ liệu mảng. http://en.wikipedia.org/wiki/Linked_list và Mảng
Hy vọng điều này giúp đỡ,
Add
luôn ở cuối mảng hiện có. List
là "đủ tốt" ở đó, ngay cả khi không phải O (1). Vấn đề nghiêm trọng xảy ra nếu bạn cần nhiều Add
s mà không phải là cuối cùng. Marc đang chỉ ra rằng nhu cầu di chuyển dữ liệu hiện có mỗi khi bạn chèn (không chỉ khi cần thay đổi kích thước) là chi phí hiệu năng đáng kể hơn List
.
Ưu điểm chính của danh sách được liên kết qua mảng là các liên kết cung cấp cho chúng tôi khả năng sắp xếp lại các mục một cách hiệu quả. Trầm tích, p. 91
Một trường hợp phổ biến để sử dụng LinkedList là như thế này:
Giả sử bạn muốn xóa nhiều chuỗi nhất định khỏi danh sách các chuỗi có kích thước lớn, giả sử là 100.000. Các chuỗi cần loại bỏ có thể được tra cứu trong Hashset dic và danh sách các chuỗi được cho là chứa từ 30.000 đến 60.000 chuỗi như vậy để loại bỏ.
Vậy thì loại Danh sách tốt nhất để lưu trữ 100.000 Chuỗi là gì? Câu trả lời là LinkedList. Nếu chúng được lưu trữ trong một ArrayList, thì việc lặp lại nó và loại bỏ các Chuỗi trùng khớp sẽ mất tới hàng tỷ thao tác, trong khi chỉ cần khoảng 100.000 thao tác bằng cách sử dụng một phương thức lặp và phương thức remove ().
LinkedList<String> strings = readStrings();
HashSet<String> dic = readDic();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String string = iterator.next();
if (dic.contains(string))
iterator.remove();
}
RemoveAll
để xóa các mục khỏi một List
mà không di chuyển nhiều mục xung quanh hoặc sử dụng Where
từ LINQ để tạo danh sách thứ hai. Sử dụng một LinkedList
ở đây tuy nhiên kết thúc tiêu thụ đáng kể bộ nhớ hơn so với các loại khác của các bộ sưu tập và mất phương tiện bộ nhớ địa phương đó nó sẽ được chú ý chậm để lặp, làm cho nó tồi tệ hơn một khá một chút List
.
RemoveAll
tương đương trong Java không.
RemoveAll
không có sẵn List
, bạn có thể thực hiện thuật toán "nén", trông giống như vòng lặp của Tom, nhưng với hai chỉ số và nhu cầu di chuyển các mục để giữ từng mục trong mảng bên trong danh sách. Hiệu quả là O (n), giống như thuật toán của Tom LinkedList
. Trong cả hai phiên bản, thời gian để tính toán khóa Hashset cho chuỗi chiếm ưu thế. Đây không phải là một ví dụ tốt về thời điểm sử dụng LinkedList
.
Khi bạn cần truy cập được lập chỉ mục tích hợp, sắp xếp (và sau khi tìm kiếm nhị phân này) và phương thức "ToArray ()", bạn nên sử dụng Danh sách.
Về cơ bản, List<>
trong .NET là một trình bao bọc trên một mảng . A LinkedList<>
là một danh sách liên kết . Vì vậy, câu hỏi đặt ra, sự khác biệt giữa một mảng và danh sách được liên kết là gì và khi nào một mảng nên được sử dụng thay vì danh sách được liên kết. Có lẽ hai yếu tố quan trọng nhất trong quyết định sử dụng của bạn sẽ thuộc về:
Điều này được chuyển thể từ Tono Nam câu trả lời được chấp nhận của , sửa một vài phép đo sai trong đó.
Các bài kiểm tra:
static void Main()
{
LinkedListPerformance.AddFirst_List(); // 12028 ms
LinkedListPerformance.AddFirst_LinkedList(); // 33 ms
LinkedListPerformance.AddLast_List(); // 33 ms
LinkedListPerformance.AddLast_LinkedList(); // 32 ms
LinkedListPerformance.Enumerate_List(); // 1.08 ms
LinkedListPerformance.Enumerate_LinkedList(); // 3.4 ms
//I tried below as fun exercise - not very meaningful, see code
//sort of equivalent to insertion when having the reference to middle node
LinkedListPerformance.AddMiddle_List(); // 5724 ms
LinkedListPerformance.AddMiddle_LinkedList1(); // 36 ms
LinkedListPerformance.AddMiddle_LinkedList2(); // 32 ms
LinkedListPerformance.AddMiddle_LinkedList3(); // 454 ms
Environment.Exit(-1);
}
Và mã:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace stackoverflow
{
static class LinkedListPerformance
{
class Temp
{
public decimal A, B, C, D;
public Temp(decimal a, decimal b, decimal c, decimal d)
{
A = a; B = b; C = c; D = d;
}
}
static readonly int start = 0;
static readonly int end = 123456;
static readonly IEnumerable<Temp> query = Enumerable.Range(start, end - start).Select(temp);
static Temp temp(int i)
{
return new Temp(i, i, i, i);
}
static void StopAndPrint(this Stopwatch watch)
{
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}
public static void AddFirst_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Insert(0, temp(i));
watch.StopAndPrint();
}
public static void AddFirst_LinkedList()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (int i = start; i < end; i++)
list.AddFirst(temp(i));
watch.StopAndPrint();
}
public static void AddLast_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Add(temp(i));
watch.StopAndPrint();
}
public static void AddLast_LinkedList()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (int i = start; i < end; i++)
list.AddLast(temp(i));
watch.StopAndPrint();
}
public static void Enumerate_List()
{
var list = new List<Temp>(query);
var watch = Stopwatch.StartNew();
foreach (var item in list)
{
}
watch.StopAndPrint();
}
public static void Enumerate_LinkedList()
{
var list = new LinkedList<Temp>(query);
var watch = Stopwatch.StartNew();
foreach (var item in list)
{
}
watch.StopAndPrint();
}
//for the fun of it, I tried to time inserting to the middle of
//linked list - this is by no means a realistic scenario! or may be
//these make sense if you assume you have the reference to middle node
//insertion to the middle of list
public static void AddMiddle_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Insert(list.Count / 2, temp(i));
watch.StopAndPrint();
}
//insertion in linked list in such a fashion that
//it has the same effect as inserting into the middle of list
public static void AddMiddle_LinkedList1()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
LinkedListNode<Temp> evenNode = null, oddNode = null;
for (int i = start; i < end; i++)
{
if (list.Count == 0)
oddNode = evenNode = list.AddLast(temp(i));
else
if (list.Count % 2 == 1)
oddNode = list.AddBefore(evenNode, temp(i));
else
evenNode = list.AddAfter(oddNode, temp(i));
}
watch.StopAndPrint();
}
//another hacky way
public static void AddMiddle_LinkedList2()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start + 1; i < end; i += 2)
list.AddLast(temp(i));
for (int i = end - 2; i >= 0; i -= 2)
list.AddLast(temp(i));
watch.StopAndPrint();
}
//OP's original more sensible approach, but I tried to filter out
//the intermediate iteration cost in finding the middle node.
public static void AddMiddle_LinkedList3()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
{
if (list.Count == 0)
list.AddLast(temp(i));
else
{
watch.Stop();
var curNode = list.First;
for (var j = 0; j < list.Count / 2; j++)
curNode = curNode.Next;
watch.Start();
list.AddBefore(curNode, temp(i));
}
}
watch.StopAndPrint();
}
}
}
Bạn có thể thấy các kết quả phù hợp với hiệu suất lý thuyết mà những người khác đã ghi lại ở đây. Khá rõ ràng - LinkedList<T>
đạt được thời gian lớn trong trường hợp chèn. Tôi đã không kiểm tra để loại bỏ từ giữa danh sách, nhưng kết quả sẽ giống nhau. Tất nhiên List<T>
có các lĩnh vực khác, nơi nó thực hiện cách tốt hơn như truy cập ngẫu nhiên O (1).
Sử dụng LinkedList<>
khi
Token Stream
.Đối với mọi thứ khác, nó là tốt hơn để sử dụng List<>
.
LinkedListNode<T>
đối tượng trong mã của mình. Nếu bạn có thể làm điều đó, thì tốt hơn nhiều so với việc sử dụng List<T>
, đặc biệt là đối với các danh sách rất dài, nơi thường xuyên chèn / xóa.
node.Value
bất cứ khi nào bạn muốn phần tử gốc). Vì vậy, bạn viết lại thuật toán để làm việc với các nút, không phải giá trị thô.
Tôi đồng ý với hầu hết các điểm được thực hiện ở trên. Và tôi cũng đồng ý rằng Danh sách có vẻ như là một lựa chọn rõ ràng hơn trong hầu hết các trường hợp.
Nhưng, tôi chỉ muốn nói thêm rằng có nhiều trường hợp trong đó LinkedList là sự lựa chọn tốt hơn nhiều so với List để có hiệu quả tốt hơn.
Hy vọng ai đó sẽ tìm thấy những ý kiến hữu ích.
Rất nhiều câu trả lời trung bình ở đây ...
Một số triển khai danh sách được liên kết sử dụng các khối cơ bản của các nút được phân bổ trước. Nếu họ không làm điều này hơn thời gian liên tục / thời gian tuyến tính thì ít liên quan hơn vì hiệu suất bộ nhớ sẽ kém và hiệu suất bộ đệm thậm chí còn tệ hơn.
Sử dụng danh sách liên kết khi
1) Bạn muốn an toàn chủ đề. Bạn có thể xây dựng các thuật toán an toàn chủ đề tốt hơn. Chi phí khóa sẽ thống trị một danh sách phong cách đồng thời.
2) Nếu bạn có một hàng đợi lớn như các cấu trúc và muốn xóa hoặc thêm bất cứ nơi nào trừ cuối mọi lúc. > Danh sách 100K tồn tại nhưng không phổ biến.
Tôi đã hỏi một câu hỏi tương tự liên quan đến hiệu suất của bộ sưu tập LinkedList và phát hiện ra Deque C # của Steven Cleary là một giải pháp. Không giống như bộ sưu tập Queue, Deque cho phép di chuyển các mục bật / tắt trước và sau. Nó tương tự như danh sách liên kết, nhưng với hiệu suất được cải thiện.
Deque
là "tương tự như danh sách được liên kết, nhưng với hiệu suất được cải thiện" . Vui lòng đủ điều kiện tuyên bố đó: Deque
là hiệu suất tốt hơn LinkedList
, cho mã cụ thể của bạn . Theo liên kết của bạn, tôi thấy rằng hai ngày sau bạn đã học được từ Ivan Stoev rằng đây không phải là sự không hiệu quả của LinkedList, mà là sự không hiệu quả trong mã của bạn. (Và ngay cả khi nó đã là một sự kém hiệu quả của LinkedList, điều đó sẽ không biện minh cho một tuyên bố chung rằng deque là hiệu quả hơn, chỉ trong những trường hợp cụ thể.)