Khi nào tôi nên sử dụng Danh sách so với Danh sách được liên kết


Câu trả lời:


107

Biên tập

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.

Câu trả lời gốc ...

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

Danh sách liên kết (3,9 giây)

        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;

Danh sách (2,4 giây)

        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)

Danh sách liên kết (51 giây)

        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;

Danh sách (7,26 giây)

        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;

Danh sách được liên kết có tham chiếu vị trí cần chèn (0,04 giây)

        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.


99
Có một lợi ích cho LinkedList trên Danh sách (đây là .net cụ thể): vì Danh sách được hỗ trợ bởi một mảng nội bộ, nên nó được phân bổ trong một khối liền kề. Nếu khối được phân bổ đó vượt quá 85000 byte kích thước, nó sẽ được phân bổ trên Heap đối tượng lớn, một thế hệ không thể nén được. 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ẹ.
JerKimball

35
Lưu ý rằng nếu bạn đang chuẩn bị rất nhiều (như về cơ bản bạn đang làm trong ví dụ trước) hoặc xóa mục nhập đầu tiên, một danh sách được liên kết sẽ luôn luôn nhanh hơn đáng kể, vì không cần phải tìm kiếm hoặc di chuyển / sao chép. Danh sách sẽ yêu cầu di chuyển mọi thứ lên một vị trí để chứa vật phẩm mới, thực hiện việc chuẩn bị thao tác O (N).
cHao

6
Tại sao vòng lặp 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.)
ruffin

7
Tôi đánh giá thấp câu trả lời này. 1) lời khuyên chung của bạn 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.
nawfal

47
Xin lỗi, nhưng câu trả lời này thực sự tồi tệ. Vui lòng KHÔNG nghe câu trả lời này. Lý do một cách ngắn gọn: Hoàn toàn sai lầm khi nghĩ rằng việc triển khai danh sách được hỗ trợ mảng là đủ ngu ngốc để thay đổi kích thước mảng trên mỗi lần chèn. Danh sách được liên kết chậm hơn tự nhiên so với danh sách được hỗ trợ mảng khi di chuyển ngang cũng như khi chèn ở hai đầu, bởi vì chỉ cần chúng tạo các đối tượng mới, trong khi danh sách được hỗ trợ mảng sử dụng bộ đệm (rõ ràng theo cả hai hướng). Các điểm chuẩn (được thực hiện kém) chỉ ra chính xác điều đó. Câu trả lời hoàn toàn không kiểm tra các trường hợp trong đó danh sách được liên kết là thích hợp hơn!
mafu

277

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ố.


4
Một lợi thế của Danh sách <> so với LinkedList <> mà tôi chưa bao giờ nghĩ đến về việc các bộ vi xử lý thực hiện bộ nhớ đệm của bộ nhớ. Mặc dù tôi không hiểu nó hoàn toàn, tác giả của bài viết trên blog này nói rất nhiều về "địa phương tham khảo", mà làm cho đi qua một mảng nhiều nhanh hơn so với đi qua một danh sách liên kết, ít nhất nếu danh sách liên kết đã trở nên hơi phân mảnh trong bộ nhớ . kjellkod.wordpress.com/2012/02/11/ cường
RenniePet

Danh sách @RenniePet được triển khai với một mảng động và các mảng là các khối bộ nhớ liền kề nhau.
Casey

2
Vì Danh sách là một mảng động, đó là lý do tại sao đôi khi thật tốt khi chỉ định dung lượng của Danh sách trong hàm tạo nếu bạn biết trước.
Cardin Lee JH

Có thể là việc triển khai C # của tất cả, mảng, Danh sách <T> và LinkedList <T> có phần không tối ưu cho một trường hợp rất quan trọng: Bạn cần một danh sách rất lớn, nối thêm (AddLast) và truyền tải tuần tự (theo một hướng) hoàn toàn ổn: Tôi muốn không thay đổi kích thước mảng để có được các khối liên tục (nó có được đảm bảo cho từng mảng, thậm chí là mảng 20 GB không?) và tôi không biết trước kích thước, nhưng tôi có thể đoán trước kích thước khối, ví dụ 100 MB để đặt trước mỗi lần trước. Đây sẽ là một thực hiện tốt. Hoặc là mảng / Danh sách tương tự như vậy, và tôi đã bỏ lỡ một điểm?
Philm

1
@Philm đó là loại kịch bản mà bạn viết shim của riêng bạn về chiến lược khối bạn đã chọn; List<T>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ử).
Marc Gravell

212

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:

Nối

  • LinkedList<T>.AddLast(item) thời gian không đổi
  • List<T>.Add(item) khấu hao thời gian không đổi, trường hợp xấu nhất tuyến tính

Chuẩn bị

  • LinkedList<T>.AddFirst(item) thời gian không đổi
  • List<T>.Insert(0, item) thời gian tuyến tính

Chèn

  • LinkedList<T>.AddBefore(node, item) thời gian không đổi
  • LinkedList<T>.AddAfter(node, item) thời gian không đổi
  • List<T>.Insert(index, item) thời gian tuyến tính

Gỡ bỏ

  • LinkedList<T>.Remove(item) thời gian tuyến tính
  • LinkedList<T>.Remove(node) thời gian không đổi
  • List<T>.Remove(item) thời gian tuyến tính
  • List<T>.RemoveAt(index) thời gian tuyến tính

Đếm

  • LinkedList<T>.Count thời gian không đổi
  • List<T>.Count thời gian không đổi

Chứa đựng

  • LinkedList<T>.Contains(item) thời gian tuyến tính
  • List<T>.Contains(item) thời gian tuyến tính

Thông thoáng

  • LinkedList<T>.Clear() thời gian tuyến tính
  • List<T>.Clear() thời gian tuyến tính

Như 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.


2
Là danh sách liên kết đếm không đổi? Tôi nghĩ rằng đó sẽ là tuyến tính?
Iain Ballard

10
@Iain, số lượng được lưu trữ trong cả hai lớp danh sách.
Drew Noakes

3
Bạn đã viết rằng "Danh sách <T> .Thêm thời gian logarit", tuy nhiên thực tế là "Không đổi" nếu dung lượng danh sách có thể lưu trữ mục mới và "Tuyến tính" nếu danh sách không có đủ không gian và mới được phân bổ lại.
aStranger

@aStranger, tất nhiên là bạn đúng. Không chắc chắn những gì tôi đã nghĩ ở trên - có lẽ thời gian trường hợp thông thường được khấu hao là logarit, điều đó không phải. Trong thực tế thời gian khấu hao là không đổi. Tôi đã không đi vào trường hợp tốt nhất / tồi tệ nhất của các hoạt động, nhằm so sánh đơn giản. Tôi nghĩ rằng hoạt động thêm là đủ đáng kể để cung cấp chi tiết này tuy nhiên. Sẽ chỉnh sửa câu trả lời. Cảm ơn.
Drew Noakes

1
@Philm, bạn có thể nên bắt đầu một câu hỏi mới và bạn không nói bạn sẽ sử dụng cấu trúc dữ liệu này như thế nào, nhưng nếu bạn nói một triệu hàng, bạn có thể thích một loại lai nào đó (danh sách được liên kết của mảng khối hoặc tương tự) để giảm phân mảnh heap, giảm chi phí bộ nhớ và tránh một đối tượng lớn duy nhất trên LOH.
Drew Noakes

118

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:

  • cập nhật con trỏ trong thành viên i-1 để trỏ đến thành viên mới
  • đặt con trỏ trong thành viên mới để trỏ đến thành viên 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.


6
Tôi sẽ thêm rằng các danh sách được liên kết có một chi phí cho mỗi mục được lưu trữ ở trên thông qua LinkedListNode để tham chiếu nút trước đó và nút tiếp theo. Phần thưởng của đó là một khối bộ nhớ liền kề không cần thiết để lưu trữ danh sách, không giống như một danh sách dựa trên mảng.
paulecoyote

3
Không phải là một khối bộ nhớ liền kề thường được nhìn thấy sao?
Jonathan Allen

7
Có, một khối liền kề đượ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 các nút mới được chèn / xóa.
jpierson

6
Nếu bạn đã từng phải làm việc với các mảng hoặc danh sách rất lớn (một danh sách chỉ bao bọc một mảng), bạn sẽ bắt đầu gặp vấn đề về bộ nhớ mặc dù dường như có rất nhiều bộ nhớ trong máy của bạn. Danh sách sử dụng chiến lược nhân đôi khi phân bổ không gian mới trong mảng bên dưới. Vì vậy, một mảng elemnt 1000000 đã đầy sẽ được sao chép vào một mảng mới với 2000000 phần tử. Mảng mới này cần được tạo trong một không gian bộ nhớ liền kề đủ lớn để chứa nó.
Andrew

1
Tôi đã có một trường hợp cụ thể trong đó tất cả những gì tôi đã làm là thêm và xóa, và lặp lại từng cái một ... ở đây danh sách được liên kết vượt trội hơn nhiều so với danh sách bình thường ..
Peter

26

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:

    • Vì vậy, thường xuyên nhất có thể. Nó nhanh và chiếm phạm vi RAM nhỏ nhất cho cùng một lượng thông tin.
    • Nếu bạn biết chính xác số lượng tế bào cần thiết
    • Nếu dữ liệu được lưu trong mảng <85000 b (85000/32 = 2656 phần tử cho dữ liệu số nguyên)
    • Nếu cần tốc độ truy cập ngẫu nhiên cao
  • Danh sách cần sử dụng:

    • Nếu cần để thêm các ô vào cuối danh sách (thường)
    • Nếu cần để thêm các ô vào đầu / giữa danh sách (KHÔNG PHẢI)
    • Nếu dữ liệu được lưu trong mảng <85000 b (85000/32 = 2656 phần tử cho dữ liệu số nguyên)
    • Nếu cần tốc độ truy cập ngẫu nhiên cao
  • LinkedList cần sử dụng:

    • Nếu cần để thêm các ô vào đầu / giữa / cuối danh sách (thường)
    • Nếu chỉ cần truy cập tuần tự (tiến / lùi)
    • Nếu bạn cần lưu các mục LỚN, nhưng số lượng vật phẩm thấp.
    • Tốt hơn không nên sử dụng cho số lượng lớn các mặt hàng, vì nó sử dụng bộ nhớ bổ sung cho các liên kết.

Thêm chi tiết:

Làm thế nào để làm gì đó Thú vị khi biết:

  1. 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.

  2. 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.

  3. 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ớ.

  4. 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.


1
Câu hỏi: Với "dữ liệu được lưu trong mảng <hoặc> 85.000 byte", bạn có nghĩa là dữ liệu trên mỗi mảng / danh sách NGUYÊN NHÂN phải không? Có thể hiểu rằng bạn có nghĩa là dữ liệu hóa của toàn bộ mảng ..
Philm

Các phần tử mảng nằm tuần tự trong bộ nhớ. Vì vậy, mỗi mảng. Tôi biết về lỗi trong bảng, sau này tôi sẽ sửa nó :) (Tôi hy vọng ....)
Andrew

Với các danh sách bị chậm khi chèn, nếu một danh sách có nhiều vòng quay (rất nhiều lần chèn / xóa) thì bộ nhớ bị chiếm bởi không gian bị xóa và nếu vậy, điều đó có làm cho "re" lại nhanh hơn không?
Cướp

18

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_listMảng

Hy vọng điều này giúp đỡ,


4
Danh sách <T> là mảng (T []), không dựa trên ArrayList. Chèn lại: thay đổi kích thước mảng không phải là vấn đề (thuật toán nhân đôi có nghĩa là hầu hết thời gian nó không phải làm điều này): vấn đề là nó phải sao chép tất cả dữ liệu hiện có trước tiên, điều này cần một chút thời gian.
Marc Gravell

2
@Marc, 'thuật toán nhân đôi "chỉ làm cho nó thành O (logN), nhưng nó vẫn tệ hơn O (1)
Ilya Ryzhenkov

2
Quan điểm của tôi là nó không phải là kích thước thay đổi gây ra nỗi đau - đó là lỗi. Vì vậy, trường hợp xấu nhất, nếu chúng ta thêm phần tử đầu tiên (zeroth) mỗi lần, thì blit phải di chuyển mọi thứ mỗi lần.
Marc Gravell

@IlyaRyzhenkov - bạn đang nghĩ về trường hợp Addluôn ở cuối mảng hiện có. Listlà "đủ 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 Adds 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.
ToolmakerSteve

Vấn đề là các ký hiệu Big O lý thuyết không nói lên toàn bộ câu chuyện. Trong khoa học máy tính mà tất cả mọi người đều quan tâm, nhưng có nhiều điều đáng quan tâm hơn thế này trong thế giới thực.
MattE

11

Ư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


1
IMO đây sẽ là câu trả lời. LinkedList được sử dụng khi một đơn hàng được đảm bảo là quan trọng.
RBaarda

1
@RBaarda: Tôi không đồng ý. Nó phụ thuộc vào mức độ chúng ta đang nói đến. Cấp độ thuật toán khác với cấp độ thực hiện máy. Để xem xét tốc độ, bạn cũng cần cái sau. Như đã chỉ ra, các mảng được thực hiện là "một đoạn" của bộ nhớ là hạn chế, bởi vì điều này có thể dẫn đến thay đổi kích thước và sắp xếp lại bộ nhớ, đặc biệt là với các mảng rất lớn. Sau khi suy nghĩ một lúc, một cấu trúc dữ liệu riêng đặc biệt, một danh sách các mảng được liên kết sẽ là một ý tưởng để kiểm soát tốt hơn tốc độ lấp đầy tuyến tính và truy cập các cấu trúc dữ liệu rất lớn.
Philm

1
@Philm - Tôi nêu lên nhận xét của bạn, nhưng tôi muốn chỉ ra rằng bạn đang mô tả một yêu cầu khác. Câu trả lời đang nói là danh sách được liên kết có lợi thế về hiệu suất cho các thuật toán liên quan đến việc sắp xếp lại các mục. Do đó, tôi giải thích nhận xét của RBaarda là đề cập đến nhu cầu thêm / xóa các mục trong khi tiếp tục duy trì một thứ tự nhất định (tiêu chí sắp xếp). Vì vậy, không chỉ là "điền tuyến tính". Vì điều này, List mất đi, vì các chỉ số là vô dụng (thay đổi mỗi khi bạn thêm một yếu tố ở bất cứ đâu nhưng ở phần đuôi).
ToolmakerSteve

4

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

6
Bạn chỉ có thể sử dụng RemoveAllđể xóa các mục khỏi một Listmà không di chuyển nhiều mục xung quanh hoặc sử dụng Wheretừ 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.
Phục vụ

@Servy, lưu ý rằng câu trả lời của @ Tom sử dụng Java. Tôi không chắc có RemoveAlltương đương trong Java không.
Arturo Torres Sánchez

3
@ ArturoTorresSánchez Vâng câu hỏi cụ thể là về .NET, vì vậy điều đó chỉ khiến câu trả lời trở nên ít phù hợp hơn nhiều.
Phục vụ

@Servy, sau đó bạn nên đề cập đến điều đó ngay từ đầu.
Arturo Torres Sánchez

Nếu RemoveAllkhô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.
ToolmakerSteve

2

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.


2

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ề:

  • Danh sách được liên kết có hiệu suất chèn / xóa tốt hơn nhiều, miễn là phần chèn / xóa không nằm trên phần tử cuối cùng trong bộ sưu tập. Điều này là do một mảng phải dịch chuyển tất cả các phần tử còn lại sau điểm chèn / loại bỏ. Tuy nhiên, nếu chèn / loại bỏ ở cuối đuôi của danh sách, sự thay đổi này là không cần thiết (mặc dù mảng có thể cần phải thay đổi kích thước, nếu vượt quá dung lượng của nó).
  • Mảng có khả năng truy cập tốt hơn nhiều. Mảng có thể được lập chỉ mục trực tiếp (trong thời gian không đổi). Danh sách liên kết phải được duyệt qua (thời gian tuyến tính).

1

Đ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).


0

Sử dụng LinkedList<>khi

  1. Bạn không biết có bao nhiêu vật thể đang đi qua cổng lũ. Ví dụ , Token Stream.
  2. Khi bạn CHỈ muốn xóa \ insert ở cuối.

Đối với mọi thứ khác, nó là tốt hơn để sử dụng List<>.


6
Tôi không thấy lý do tại sao điểm 2 có ý nghĩa. Danh sách được liên kết là tuyệt vời khi bạn thực hiện nhiều thao tác chèn / xóa trong toàn bộ danh sách.
vẽ Noakes

Vì thực tế là LinkedLists không dựa trên Index, bạn thực sự phải quét toàn bộ danh sách để chèn hoặc xóa mà phải chịu hình phạt O (n). Danh sách <> mặt khác bị thay đổi kích thước mảng, nhưng IMO vẫn là một lựa chọn tốt hơn khi so sánh với LinkedLists.
Antony Thomas

1
Bạn không phải quét danh sách để chèn / xóa nếu bạn theo dõi các 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.
vẽ Noakes

Bạn có nghĩa là thông qua một hashtable? Nếu đó là trường hợp, đó sẽ là sự đánh đổi không gian điển hình mà mọi lập trình viên máy tính nên đưa ra lựa chọn dựa trên miền vấn đề :) Nhưng vâng, điều đó sẽ làm cho nó nhanh hơn.
Antony Thomas

1
@AntonyThomas - Không, anh ta có nghĩa là bằng cách chuyển các tham chiếu đến các nút thay vì chuyển xung quanh các tham chiếu đến các phần tử . Nếu tất cả những gì bạn có là một phần tử , thì cả List và LinkedList đều có hiệu năng kém, vì bạn phải tìm kiếm. Nếu bạn nghĩ "nhưng với Danh sách, tôi chỉ có thể chuyển vào một chỉ mục": điều đó chỉ hợp lệ khi bạn không bao giờ chèn một phần tử mới vào giữa Danh sách. LinkedList không có giới hạn này, nếu bạn giữ một nút (và sử dụng node.Valuebấ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ô.
ToolmakerSteve

0

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.

  1. Giả sử bạn đang duyệt qua các yếu tố và bạn muốn thực hiện nhiều thao tác chèn / xóa; LinkedList thực hiện nó trong thời gian O (n) tuyến tính, trong khi List thực hiện theo thời gian O (n ^ 2) bậc hai.
  2. Giả sử bạn muốn truy cập các đối tượng lớn hơn nhiều lần, LinkedList trở nên rất hữu ích.
  3. Deque () và queue () được triển khai tốt hơn bằng LinkedList.
  4. Việc tăng kích thước của LinkedList sẽ dễ dàng và tốt hơn nhiều khi bạn đang xử lý nhiều đối tượng lớn hơn.

Hy vọng ai đó sẽ tìm thấy những ý kiến ​​hữu ích.


Lưu ý rằng lời khuyên này là dành cho .NET, không phải Java. Trong triển khai danh sách được liên kết của Java, bạn không có khái niệm về "nút hiện tại", do đó bạn phải duyệt qua danh sách cho mỗi lần chèn.
Jonathan Allen

Câu trả lời này chỉ đúng một phần: 2) nếu các phần tử lớn, sau đó làm cho phần tử loại Class không phải là Struct, để Danh sách chỉ giữ một tham chiếu. Sau đó kích thước phần tử trở nên không liên quan. 3) Deque và hàng đợi có thể được thực hiện một cách hiệu quả trong Danh sách nếu bạn sử dụng Danh sách làm "bộ đệm tròn", thay vì thực hiện chèn hoặc xóa khi bắt đầu. StephenCleary Deque . 4) đúng một phần: khi nhiều đối tượng, pro của LL không cần bộ nhớ tiếp giáp lớn; Nhược điểm là bộ nhớ thêm cho con trỏ nút.
ToolmakerSteve

-2

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.


3
Câu hỏi này là về hai triển khai C #, không phải là danh sách liên kết nói chung.
Jonathan Allen

Giống nhau ở mọi ngôn ngữ
user1496062 7/12/18

-2

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.


1
Re tuyên bố của bạn đó Deque "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ố đó: Dequelà 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ể.)
ToolmakerSteve
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.